littlejsengine 1.18.1 → 1.18.4

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.
@@ -35,7 +35,7 @@ const engineName = 'LittleJS';
35
35
  * @type {string}
36
36
  * @default
37
37
  * @memberof Engine */
38
- const engineVersion = '1.18.1';
38
+ const engineVersion = '1.18.4';
39
39
 
40
40
  /** Frames per second to update
41
41
  * @type {number}
@@ -69,7 +69,7 @@ let frame = 0;
69
69
  * @memberof Engine */
70
70
  let time = 0;
71
71
 
72
- /** Actual clock time since start in seconds (not affected by pause or frame rate clamping)
72
+ /** Actual clock time since start in seconds (not affected by pause, timescale, or frame rate clamping)
73
73
  * @type {number}
74
74
  * @memberof Engine */
75
75
  let timeReal = 0;
@@ -200,11 +200,14 @@ async function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, game
200
200
  averageFPS = lerp(averageFPS, 1e3/(frameTimeDeltaMS||1), .05);
201
201
  const debugSpeedUp = debug && keyIsDown('Equal'); // +
202
202
  const debugSpeedDown = debug && keyIsDown('Minus'); // -
203
- if (debug) // +/- to speed/slow time
204
- frameTimeDeltaMS *= debugSpeedUp ? 10 : debugSpeedDown ? .1 : 1;
205
- timeReal += frameTimeDeltaMS / 1e3;
203
+ const debugScale = debugSpeedUp ? 10 : debugSpeedDown ? .1 : 1;
204
+
205
+ // apply time deltas
206
+ timeReal += frameTimeDeltaMS * debugScale / 1e3;
207
+ const combinedScale = timeScale * debugScale;
208
+ frameTimeDeltaMS *= combinedScale;
206
209
  frameTimeBufferMS += paused ? 0 : frameTimeDeltaMS;
207
- if (!debugSpeedUp)
210
+ if (combinedScale <= 1)
208
211
  frameTimeBufferMS = min(frameTimeBufferMS, 50); // clamp min framerate
209
212
 
210
213
  let wasUpdated = false;
@@ -2800,6 +2803,18 @@ let cameraAngle = 0;
2800
2803
  * @memberof Settings */
2801
2804
  let cameraScale = 32;
2802
2805
 
2806
+ ///////////////////////////////////////////////////////////////////////////////
2807
+ // Time settings
2808
+
2809
+ /** Scale applied to engine time, can be used for slow motion or fast forward
2810
+ * - 1 is normal speed, 2 is double speed, 0.5 is half speed
2811
+ * - 0 freezes the simulation without setting the paused flag
2812
+ * - Should be >= 0; stacks multiplicatively with the debug +/- shortcut
2813
+ * @type {number}
2814
+ * @default
2815
+ * @memberof Settings */
2816
+ let timeScale = 1;
2817
+
2803
2818
  ///////////////////////////////////////////////////////////////////////////////
2804
2819
  // Display settings
2805
2820
 
@@ -3003,18 +3018,22 @@ let touchInputEnable = true;
3003
3018
 
3004
3019
  /** True if touch gamepad should appear on mobile devices
3005
3020
  * - Supports left analog stick, 4 face buttons and start button (button 9)
3021
+ * - setTouchGamepadButtonCount(1) to use face buttons as right analog stick
3022
+ * - Analog stick buttons 10 and 11 are also activated when virtual sticks are touched
3023
+
3006
3024
  * @type {boolean}
3007
3025
  * @default
3008
3026
  * @memberof Settings */
3009
3027
  let touchGamepadEnable = false;
3010
3028
 
3011
3029
  /** True if touch gamepad should have start button in the center
3030
+ * - Prevents activating if overlappng with virtual stick or buttons if they are enabled
3012
3031
  * - When the game is paused, any touch will press the button
3013
- * - This can function as a way to pause/unpause the game
3014
- * @type {boolean}
3032
+ * - Set size to enable the center button
3033
+ * @type {number}
3015
3034
  * @default
3016
3035
  * @memberof Settings */
3017
- let touchGamepadCenterButton = true;
3036
+ let touchGamepadCenterButtonSize = 300;
3018
3037
 
3019
3038
  /** Number of buttons on touch gamepad (0-4), if 1 also acts as right analog stick
3020
3039
  * @type {number}
@@ -3032,7 +3051,7 @@ let touchGamepadAnalog = true;
3032
3051
  * @type {number}
3033
3052
  * @default
3034
3053
  * @memberof Settings */
3035
- let touchGamepadSize = 99;
3054
+ let touchGamepadSize = 100;
3036
3055
 
3037
3056
  /** Transparency of touch gamepad overlay
3038
3057
  * @type {number}
@@ -3124,6 +3143,11 @@ function setCameraAngle(angle) { cameraAngle = angle; }
3124
3143
  * @memberof Settings */
3125
3144
  function setCameraScale(scale) { cameraScale = scale; }
3126
3145
 
3146
+ /** Set scale applied to engine time
3147
+ * @param {number} scale
3148
+ * @memberof Settings */
3149
+ function setTimeScale(scale) { timeScale = scale; }
3150
+
3127
3151
  /** Set if tiles should be colorized when using canvas2d
3128
3152
  * This can be slower but results should look nearly identical to WebGL rendering
3129
3153
  * It can be enabled/disabled at any time
@@ -3300,11 +3324,12 @@ function setTouchInputEnable(enable) { touchInputEnable = enable; }
3300
3324
  * @memberof Settings */
3301
3325
  function setTouchGamepadEnable(enable) { touchGamepadEnable = enable; }
3302
3326
 
3303
- /** True if touch gamepad should have start button in the center
3304
- * - This can function as a way to pause/unpause the game
3305
- * @param {boolean} enable
3327
+ /** Set if touch gamepad should have start button in the center
3328
+ * - Set size to enable the center button
3329
+ * - When the game is paused, any touch will press the button
3330
+ * @param {number} size
3306
3331
  * @memberof Settings */
3307
- function setTouchGamepadCenterButton(enable) { touchGamepadCenterButton = enable; }
3332
+ function setTouchGamepadCenterButtonSize(size) { touchGamepadCenterButtonSize = size; }
3308
3333
 
3309
3334
  /** Set number of buttons on touch gamepad (0-4), if 1 also acts as right analog stick
3310
3335
  * @param {number} count
@@ -3842,6 +3867,7 @@ class EngineObject
3842
3867
  child.parent = this;
3843
3868
  child.localPos = localPos.copy();
3844
3869
  child.localAngle = localAngle;
3870
+ child.updateTransforms();
3845
3871
  return child;
3846
3872
  }
3847
3873
 
@@ -3996,6 +4022,12 @@ let textureInfos = [];
3996
4022
  * @memberof Draw */
3997
4023
  let drawCount;
3998
4024
 
4025
+ // internal predicates for tint short-circuiting in canvas2D draw paths
4026
+ // isWhite ignores alpha because alpha is applied via globalAlpha, not multiply
4027
+ // isBlack includes alpha so additive colors that only contribute alpha are not skipped
4028
+ /** @param {Color} c */ function isWhite(c) { return c.r >= 1 && c.g >= 1 && c.b >= 1; }
4029
+ /** @param {Color} c */ function isBlack(c) { return c.r <= 0 && c.g <= 0 && c.b <= 0 && c.a <= 0; }
4030
+
3999
4031
  ///////////////////////////////////////////////////////////////////////////////
4000
4032
 
4001
4033
  /**
@@ -4325,6 +4357,96 @@ function drawRectGradient(pos, size, colorTop=WHITE, colorBottom=BLACK, angle=0,
4325
4357
  }
4326
4358
  }
4327
4359
 
4360
+ /** Draw a texture tiled (wrapped) across a rectangle in world space.
4361
+ * Useful for backgrounds, repeating patterns, and seamless fills.
4362
+ * The whole texture is tiled — sub-region (TileInfo) wrapping is not supported.
4363
+ * @param {Vector2} pos - Center of the rect in world space
4364
+ * @param {Vector2} size - Size of the rect in world space
4365
+ * @param {Vector2} wrapCount - How many times the texture repeats (x, y)
4366
+ * @param {TextureInfo|number} [texture=0] - TextureInfo or texture index into textureInfos
4367
+ * @param {Color} [color=WHITE] - Color to modulate with
4368
+ * @param {number} [angle=0] - Angle to rotate by
4369
+ * @param {Color} [additiveColor] - Additive color to be applied if any
4370
+ * @param {boolean} [useWebGL=glEnable] - Use accelerated WebGL rendering?
4371
+ * @param {boolean} [screenSpace=false] - Are pos and size in screen space?
4372
+ * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context] - Canvas 2D context to draw to
4373
+ * @memberof Draw */
4374
+ function drawTextureWrapped(pos, size, wrapCount, texture=0, color=WHITE,
4375
+ angle=0, additiveColor, useWebGL=glEnable, screenSpace=false, context)
4376
+ {
4377
+ ASSERT(isVector2(pos), 'pos must be a vec2');
4378
+ ASSERT(isVector2(size), 'size must be a vec2');
4379
+ ASSERT(isVector2(wrapCount), 'wrapCount must be a vec2');
4380
+ ASSERT(isColor(color), 'color is invalid');
4381
+ ASSERT(isNumber(angle), 'angle must be a number');
4382
+ ASSERT(!additiveColor || isColor(additiveColor), 'additiveColor must be a color');
4383
+ ASSERT(!context || !useWebGL, 'context only supported in canvas 2D mode');
4384
+ ASSERT(!(texture instanceof TileInfo),
4385
+ 'pass a TextureInfo or texture index, not a TileInfo — use tileInfo.textureInfo');
4386
+
4387
+ // short-circuit before texture lookup — textureInfos[0] is undefined in headless mode
4388
+ if (headlessMode) return;
4389
+
4390
+ // resolve texture argument: TextureInfo or index
4391
+ const textureInfo = typeof texture === 'number' ? textureInfos[texture] : texture;
4392
+ ASSERT(textureInfo instanceof TextureInfo, 'texture not loaded');
4393
+ ASSERT(textureInfo.size.x > 0, 'texture not loaded');
4394
+
4395
+ if (useWebGL && glEnable)
4396
+ {
4397
+ ASSERT(!!glContext, 'WebGL is not enabled!');
4398
+ if (screenSpace)
4399
+ [pos, size, angle] = screenToWorldTransform(pos, size, angle);
4400
+ glSetTexture(textureInfo.glTexture);
4401
+ glDraw(pos.x, pos.y, size.x, size.y, angle,
4402
+ 0, 0, wrapCount.x, wrapCount.y,
4403
+ color.rgbaInt(), additiveColor && additiveColor.rgbaInt());
4404
+ return;
4405
+ }
4406
+
4407
+ // Canvas2D path — increment drawCount here (WebGL batch counts via glBatchCount)
4408
+ ++drawCount;
4409
+
4410
+ if (!screenSpace)
4411
+ {
4412
+ pos = worldToScreen(pos);
4413
+ size = size.scale(cameraScale);
4414
+ angle -= cameraAngle;
4415
+ }
4416
+
4417
+ // pick image source: raw, or tinted bake. Match drawImageColor's
4418
+ // "no tint needed" predicate so behavior stays consistent.
4419
+ const noTint = !canvasColorTiles ||
4420
+ (additiveColor
4421
+ ? isWhite(color.add(additiveColor)) && additiveColor.a <= 0
4422
+ : isWhite(color));
4423
+ // alpha is baked into pixels by bakeTintedImage's additive branch;
4424
+ // in that case globalAlpha must NOT also apply color.a
4425
+ const alphaBaked = !noTint && additiveColor && !isBlack(additiveColor);
4426
+ const source = noTint
4427
+ ? textureInfo.image
4428
+ : bakeTintedImage(textureInfo.image, color, additiveColor);
4429
+
4430
+ context = context || drawContext;
4431
+ context.save();
4432
+ context.translate(pos.x + .5, pos.y + .5);
4433
+ context.rotate(angle);
4434
+ context.globalAlpha = alphaBaked ? 1 : color.a;
4435
+
4436
+ const pattern = context.createPattern(source, 'repeat');
4437
+ // map pattern-source pixels into user space so the rect contains
4438
+ // wrapCount.x × wrapCount.y repeats
4439
+ const m = new DOMMatrix()
4440
+ .translate(-size.x/2, -size.y/2)
4441
+ .scale(size.x / (wrapCount.x * source.width),
4442
+ size.y / (wrapCount.y * source.height));
4443
+ pattern.setTransform(m);
4444
+ context.fillStyle = pattern;
4445
+ context.fillRect(-size.x/2, -size.y/2, size.x, size.y);
4446
+ context.globalAlpha = 1;
4447
+ context.restore();
4448
+ }
4449
+
4328
4450
  /** Draw connected lines between a series of points
4329
4451
  * @param {Array<Vector2>} points
4330
4452
  * @param {number} [width]
@@ -4848,6 +4970,43 @@ function combineCanvases()
4848
4970
  mainContext.drawImage(workCanvas, 0, 0);
4849
4971
  }
4850
4972
 
4973
+ // Internal: bake a color/additive-color tint into workReadCanvas at the
4974
+ // image's native resolution. Returns the work canvas, suitable for
4975
+ // passing to context.createPattern. Used by drawTextureWrapped's
4976
+ // Canvas2D path. Caller is responsible for short-circuiting when no
4977
+ // tint is needed (i.e. color is white and additiveColor is black/none).
4978
+ function bakeTintedImage(image, color, additiveColor)
4979
+ {
4980
+ const w = image.width|0, h = image.height|0;
4981
+ workReadCanvas.width = w;
4982
+ workReadCanvas.height = h;
4983
+ workReadContext.drawImage(image, 0, 0);
4984
+
4985
+ const imageData = workReadContext.getImageData(0, 0, w, h);
4986
+ const data = imageData.data;
4987
+ if (additiveColor && !isBlack(additiveColor))
4988
+ {
4989
+ // multiply + additive (slower)
4990
+ const colorMultiply = [color.r, color.g, color.b, color.a];
4991
+ const colorAdd = [additiveColor.r * 255, additiveColor.g * 255,
4992
+ additiveColor.b * 255, additiveColor.a * 255];
4993
+ for (let i = 0; i < data.length; ++i)
4994
+ data[i] = data[i] * colorMultiply[i&3] + colorAdd[i&3] |0;
4995
+ }
4996
+ else
4997
+ {
4998
+ // RGB only, faster — alpha left intact for the caller
4999
+ for (let i = 0; i < data.length; i+=4)
5000
+ {
5001
+ data[i ] *= color.r;
5002
+ data[i+1] *= color.g;
5003
+ data[i+2] *= color.b;
5004
+ }
5005
+ }
5006
+ workReadContext.putImageData(imageData, 0, 0);
5007
+ return workReadCanvas;
5008
+ }
5009
+
4851
5010
  /** Helper function to draw an image with color and additive color applied
4852
5011
  * This is slower then normal drawImage when color is applied
4853
5012
  * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} context
@@ -4866,8 +5025,6 @@ function combineCanvases()
4866
5025
  * @memberof Draw */
4867
5026
  function drawImageColor(context, image, sx, sy, sWidth, sHeight, dx, dy, dWidth, dHeight, color, additiveColor, bleed=0)
4868
5027
  {
4869
- function isWhite(c) { return c.r >= 1 && c.g >= 1 && c.b >= 1; }
4870
- function isBlack(c) { return c.r <= 0 && c.g <= 0 && c.b <= 0 && c.a <= 0; }
4871
5028
  const sx2 = bleed;
4872
5029
  const sy2 = bleed;
4873
5030
  sWidth = max(1,sWidth|0);
@@ -5336,6 +5493,32 @@ function gamepadStickCount(gamepad=gamepadPrimary)
5336
5493
  return gamepadStickData[gamepad]?.length ?? 0;
5337
5494
  }
5338
5495
 
5496
+ /** Pulse a gamepad's vibration hardware using the dual-rumble effect if it exists
5497
+ * Strong magnitude is usually the left side motor, weak magnitude is usually the right side motor
5498
+ * @param {number} [gamepad] - gamepad index
5499
+ * @param {number} [duration] - effect duration in ms
5500
+ * @param {number} [strongMagnitude] - strong (left) motor intensity, 0 to 1
5501
+ * @param {number} [weakMagnitude] - weak (right) motor intensity, 0 to 1
5502
+ * @param {number} [startDelay] - delay in ms before the effect starts
5503
+ * @memberof Input */
5504
+ function gamepadVibrate(gamepad=gamepadPrimary, duration=200, strongMagnitude=1, weakMagnitude=1, startDelay=0)
5505
+ {
5506
+ ASSERT(isNumber(gamepad), 'gamepad must be a number');
5507
+ if (!vibrateEnable || headlessMode) return;
5508
+ const pad = navigator?.getGamepads?.()[gamepad];
5509
+ pad?.vibrationActuator?.playEffect?.('dual-rumble', {duration, strongMagnitude, weakMagnitude, startDelay});
5510
+ }
5511
+
5512
+ /** Stop vibration on a gamepad
5513
+ * @memberof Input */
5514
+ function gamepadVibrateStop(gamepad=gamepadPrimary)
5515
+ {
5516
+ ASSERT(isNumber(gamepad), 'gamepad must be a number');
5517
+ if (!vibrateEnable || headlessMode) return;
5518
+ const pad = navigator?.getGamepads?.()[gamepad];
5519
+ pad?.vibrationActuator?.reset?.();
5520
+ }
5521
+
5339
5522
  ///////////////////////////////////////////////////////////////////////////////
5340
5523
 
5341
5524
  /** Pulse the vibration hardware if it exists
@@ -5573,7 +5756,7 @@ function inputInit()
5573
5756
  if (touching)
5574
5757
  {
5575
5758
  touchGamepadTimer.set();
5576
- if (touchGamepadCenterButton && !wasTouching && paused)
5759
+ if (touchGamepadCenterButtonSize && !wasTouching && paused)
5577
5760
  {
5578
5761
  // touch anywhere to press start when paused
5579
5762
  touchGamepadButtons[9] = 1;
@@ -5598,6 +5781,7 @@ function inputInit()
5598
5781
  // virtual analog stick
5599
5782
  const delta = touchPos.subtract(stickCenter);
5600
5783
  touchGamepadSticks[0] = delta.scale(2/touchGamepadSize).clampLength();
5784
+ touchGamepadButtons[10] = 1; // also press a button when touching stick
5601
5785
  }
5602
5786
  else if (buttonCenter.distance(touchPos) < touchGamepadSize)
5603
5787
  {
@@ -5606,6 +5790,7 @@ function inputInit()
5606
5790
  // virtual right analog stick
5607
5791
  const delta = touchPos.subtract(buttonCenter);
5608
5792
  touchGamepadSticks[1] = delta.scale(2/touchGamepadSize).clampLength();
5793
+ touchGamepadButtons[11] = 1; // also press a button when touching right stick
5609
5794
  }
5610
5795
  // virtual face buttons
5611
5796
  let button = buttonCenter.subtract(touchPos).direction();
@@ -5622,8 +5807,7 @@ function inputInit()
5622
5807
  if (button < touchGamepadButtonCount)
5623
5808
  touchGamepadButtons[button] = 1;
5624
5809
  }
5625
- else if (touchGamepadCenterButton &&
5626
- startCenter.distance(touchPos) < touchGamepadSize)
5810
+ else if (startCenter.distance(touchPos) < touchGamepadCenterButtonSize)
5627
5811
  {
5628
5812
  // virtual start button in center
5629
5813
  touchGamepadButtons[9] = 1;
@@ -5672,6 +5856,18 @@ function inputUpdate()
5672
5856
  // update touch gamepad if enabled
5673
5857
  if (touchGamepadEnable && isTouchDevice)
5674
5858
  {
5859
+ if (debugGamepads)
5860
+ {
5861
+ const stickCenter = vec2(touchGamepadSize, mainCanvasSize.y-touchGamepadSize);
5862
+ const buttonCenter = touchGamepadButtonCenter();
5863
+ const startCenter = mainCanvasSize.scale(.5);
5864
+
5865
+ debugCircle(stickCenter, 2*touchGamepadSize, 'cyan', 0, false, true);
5866
+ debugCircle(buttonCenter, 2*touchGamepadSize, 'cyan', 0, false, true);
5867
+ if (touchGamepadCenterButtonSize)
5868
+ debugCircle(startCenter, 2*touchGamepadCenterButtonSize, 'cyan', 0, false, true);
5869
+ }
5870
+
5675
5871
  if (!touchGamepadTimer.isSet()) return;
5676
5872
 
5677
5873
  // read virtual analog stick
@@ -5699,7 +5895,7 @@ function inputUpdate()
5699
5895
 
5700
5896
  // read virtual gamepad buttons
5701
5897
  const data = inputData[1] ?? (inputData[1] = []);
5702
- for (let i=10; i--;)
5898
+ for (let i=12; i--;)
5703
5899
  {
5704
5900
  const wasDown = gamepadIsDown(i,0);
5705
5901
  data[i] = touchGamepadButtons[i] ? wasDown ? 1 : 3 : wasDown ? 4 : 0;
@@ -8298,9 +8494,8 @@ function glClearCanvas()
8298
8494
  /** Set the WebGL texture, called automatically if using multiple textures
8299
8495
  * - This may also flush the gl buffer resulting in more draw calls and worse performance
8300
8496
  * @param {WebGLTexture} texture
8301
- * @param {boolean} [wrap] - Should the texture wrap or clamp
8302
8497
  * @memberof WebGL */
8303
- function glSetTexture(texture, wrap=false)
8498
+ function glSetTexture(texture)
8304
8499
  {
8305
8500
  // must flush cache with the old texture to set a new one
8306
8501
  if (!glContext || texture === glActiveTexture) return;
@@ -8308,11 +8503,6 @@ function glSetTexture(texture, wrap=false)
8308
8503
  glFlush();
8309
8504
  glActiveTexture = texture;
8310
8505
  glContext.bindTexture(glContext.TEXTURE_2D, glActiveTexture);
8311
-
8312
- // set wrap mode
8313
- const wrapMode = wrap ? glContext.REPEAT : glContext.CLAMP_TO_EDGE;
8314
- glContext.texParameteri(glContext.TEXTURE_2D, glContext.TEXTURE_WRAP_S, wrapMode);
8315
- glContext.texParameteri(glContext.TEXTURE_2D, glContext.TEXTURE_WRAP_T, wrapMode);
8316
8506
  }
8317
8507
 
8318
8508
  /** Compile WebGL shader of the given type, will throw errors if in debug mode
@@ -8387,6 +8577,8 @@ function glCreateTexture(image)
8387
8577
  const minFilter = mipMap ? glContext.LINEAR_MIPMAP_LINEAR : magFilter;
8388
8578
  glContext.texParameteri(glContext.TEXTURE_2D, glContext.TEXTURE_MAG_FILTER, magFilter);
8389
8579
  glContext.texParameteri(glContext.TEXTURE_2D, glContext.TEXTURE_MIN_FILTER, minFilter);
8580
+ glContext.texParameteri(glContext.TEXTURE_2D, glContext.TEXTURE_WRAP_S, glContext.REPEAT);
8581
+ glContext.texParameteri(glContext.TEXTURE_2D, glContext.TEXTURE_WRAP_T, glContext.REPEAT);
8390
8582
  if (mipMap)
8391
8583
  glContext.generateMipmap(glContext.TEXTURE_2D);
8392
8584
 
@@ -10303,7 +10495,7 @@ class UIObject
10303
10495
 
10304
10496
  /** Internal function called when object is clicked
10305
10497
  * @param {boolean} [playSound] */
10306
- click(playSound)
10498
+ click(playSound=true)
10307
10499
  {
10308
10500
  this.onClick();
10309
10501
  if (playSound && this.soundClick)
@@ -11276,7 +11468,8 @@ class Box2dObject extends EngineObject
11276
11468
  {
11277
11469
  this.pos = pos;
11278
11470
  this.angle = angle;
11279
- this.body.SetTransform(box2d.vec2dTo(pos), angle);
11471
+ // box2d uses reverse angle
11472
+ this.body.SetTransform(box2d.vec2dTo(pos), -angle);
11280
11473
  }
11281
11474
 
11282
11475
  /** Sets the position
@@ -11287,7 +11480,7 @@ class Box2dObject extends EngineObject
11287
11480
  /** Sets the angle
11288
11481
  * @param {number} angle */
11289
11482
  setAngle(angle)
11290
- { this.setTransform(box2d.vec2From(this.body.GetPosition()), -angle); }
11483
+ { this.setTransform(box2d.vec2From(this.body.GetPosition()), angle); }
11291
11484
 
11292
11485
  /** Sets the linear velocity
11293
11486
  * @param {Vector2} velocity */
@@ -12832,6 +13025,7 @@ async function box2dInit()
12832
13025
  {
12833
13026
  if (o.body)
12834
13027
  {
13028
+ // box2d uses reverse angle
12835
13029
  o.pos = box2d.vec2From(o.body.GetPosition());
12836
13030
  o.angle = -o.body.GetAngle();
12837
13031
  }
@@ -13042,6 +13236,514 @@ function drawThreeSlice(pos, size, startTile, color, borderSize=1, additiveColor
13042
13236
  drawTile(pos.add(cornerPos.rotate(rotateAngle)), cornerSize, cornerTile, color, a, false, additiveColor, useWebGL, screenSpace, context);
13043
13237
  }
13044
13238
  }
13239
+ /**
13240
+ * LittleJS Tween System Plugin
13241
+ * - Lightweight tweens for numbers, Vector2, Color, or any .lerp-able type
13242
+ * - Chainable easing, looping, and ping-pong
13243
+ * - Property-path helper for the common case of animating an object field
13244
+ * - Auto-updates via engineAddPlugin; pauses with the game by default
13245
+ * @namespace TweenSystem
13246
+ */
13247
+
13248
+ ///////////////////////////////////////////////////////////////////////////////
13249
+
13250
+ // Module-private list of tweens currently running.
13251
+ const tweenActive = [];
13252
+
13253
+ // Time tracking for delta computation between engine plugin calls.
13254
+ let lastTime = 0;
13255
+ let lastTimeReal = 0;
13256
+
13257
+ // True if the value is an instance of a class that exposes a numeric-percent
13258
+ // `lerp(other, percent)` method (Vector2, Color, or any future class).
13259
+ function isLerpable(v) { return v && typeof v.lerp === 'function'; }
13260
+
13261
+ ///////////////////////////////////////////////////////////////////////////////
13262
+
13263
+ /** A numeric tween: drives a callback with a value interpolated between
13264
+ * `start` and `end` over `duration` seconds. Pauses with the game by default.
13265
+ * @memberof TweenSystem
13266
+ * @example
13267
+ * // Animate a fade-out over 2 seconds with an ease-out sine curve.
13268
+ * new Tween((v) => obj.alpha = v, 1, 0, 2, { ease: Ease.OUT(Ease.SINE) });
13269
+ */
13270
+ class Tween
13271
+ {
13272
+ /** Create a new tween. The callback fires immediately with `start` so the
13273
+ * target snaps to the start value on the same frame the tween is created.
13274
+ *
13275
+ * `start` and `end` may be numbers, Vector2 instances, Color instances, or
13276
+ * any object exposing a `lerp(other, percent) => sameType` method. The
13277
+ * callback receives the interpolated value (a number, or a fresh instance
13278
+ * for lerp-able types). Both endpoints must be the same type.
13279
+ * @param {function(number|Vector2|Color):void} callback - Called with the interpolated value each frame
13280
+ * @param {number|Vector2|Color} [start=0] - Starting value
13281
+ * @param {number|Vector2|Color} [end=1] - Ending value
13282
+ * @param {number} [duration=1] - Duration in seconds
13283
+ * @param {Object} [options]
13284
+ * @param {function(number):number} [options.ease] - Easing function (defaults to LINEAR)
13285
+ * @param {boolean} [options.useRealTime=false] - Advance even when the game is paused (matches Timer's useRealTime)
13286
+ * @param {boolean} [options.paused=false] - Start in paused state */
13287
+ constructor(callback, start = 0, end = 1, duration = 1, options = {})
13288
+ {
13289
+ ASSERT(typeof callback === 'function', 'Tween callback must be a function');
13290
+ if (isLerpable(start))
13291
+ {
13292
+ ASSERT(start.constructor === end.constructor,
13293
+ 'Tween start and end must be the same type');
13294
+ }
13295
+ else
13296
+ {
13297
+ ASSERT(isNumber(start), 'Tween start must be a number or have a .lerp method');
13298
+ ASSERT(isNumber(end), 'Tween end must be a number when start is a number');
13299
+ }
13300
+ ASSERT(isNumber(duration) && duration > 0, 'Tween duration must be > 0');
13301
+
13302
+ this.callback = callback;
13303
+ this.start = start;
13304
+ this.end = end;
13305
+ this.duration = duration;
13306
+ this.life = duration;
13307
+ this.ease = options.ease || Ease.LINEAR;
13308
+ this.useRealTime = !!options.useRealTime;
13309
+ this.paused = !!options.paused;
13310
+
13311
+ /** @private completion callback set by then(), loop(), pingPong(). */
13312
+ this.thenCallback = undefined;
13313
+ /** @private remaining iterations including the current run (loop/pingPong only). */
13314
+ this.loopRemaining = 0;
13315
+
13316
+ tweenActive.push(this);
13317
+ // Snap target to start immediately.
13318
+ callback(this.interp(duration));
13319
+ }
13320
+
13321
+ /** Set the easing curve and return this for chaining.
13322
+ * @param {function(number):number} easeFn
13323
+ * @returns {Tween}
13324
+ * @memberof TweenSystem */
13325
+ setEase(easeFn)
13326
+ {
13327
+ this.ease = easeFn;
13328
+ return this;
13329
+ }
13330
+
13331
+ /** Set a single completion callback. Calling `then` again replaces the
13332
+ * previous callback. Returns this for chaining.
13333
+ *
13334
+ * Calling `then` after `loop` or `pingPong` overrides the loop chain
13335
+ * (last call wins).
13336
+ * @param {function():void} callback
13337
+ * @returns {Tween}
13338
+ * @memberof TweenSystem */
13339
+ then(callback)
13340
+ {
13341
+ this.thenCallback = callback;
13342
+ this.loopRemaining = 0;
13343
+ return this;
13344
+ }
13345
+
13346
+ /** Repeat this tween `n` total times. After each iteration finishes, a
13347
+ * fresh tween with the same parameters takes over via the `then` slot.
13348
+ * `loop()` with no argument loops forever.
13349
+ *
13350
+ * Mutually exclusive with `pingPong`; calling either replaces the other,
13351
+ * and calling `then` after either clears the loop (last call wins).
13352
+ * @param {number} [count=Infinity]
13353
+ * @returns {Tween}
13354
+ * @memberof TweenSystem */
13355
+ loop(count = Infinity)
13356
+ {
13357
+ this.loopRemaining = count;
13358
+ this.thenCallback = () => loopContinuation(this);
13359
+ return this;
13360
+ }
13361
+
13362
+ /** Like `loop`, but swap `start` and `end` between iterations so the value
13363
+ * bounces back and forth. `pingPong()` with no argument bounces forever.
13364
+ *
13365
+ * Mutually exclusive with `loop`; calling either replaces the other, and
13366
+ * calling `then` after either clears the loop (last call wins).
13367
+ * @param {number} [count=Infinity]
13368
+ * @returns {Tween}
13369
+ * @memberof TweenSystem */
13370
+ pingPong(count = Infinity)
13371
+ {
13372
+ this.loopRemaining = count;
13373
+ this.thenCallback = () => pingPongContinuation(this);
13374
+ return this;
13375
+ }
13376
+
13377
+ /** Pause this tween. While paused, tweenUpdate skips it.
13378
+ * @memberof TweenSystem */
13379
+ pause() { this.paused = true; }
13380
+
13381
+ /** Resume a paused tween.
13382
+ * @memberof TweenSystem */
13383
+ resume() { this.paused = false; }
13384
+
13385
+ /** Reset this tween to the start: life back to duration, pause cleared,
13386
+ * re-added to the active list if previously stopped, and the callback
13387
+ * re-fired with the start value.
13388
+ * @memberof TweenSystem */
13389
+ restart()
13390
+ {
13391
+ this.life = this.duration;
13392
+ this.paused = false;
13393
+ if (tweenActive.indexOf(this) < 0) tweenActive.push(this);
13394
+ this.callback(this.interp(this.duration));
13395
+ }
13396
+
13397
+ /** True if this tween is in the active list and not paused.
13398
+ * @returns {boolean}
13399
+ * @memberof TweenSystem */
13400
+ isActive()
13401
+ {
13402
+ return !this.paused && tweenActive.indexOf(this) >= 0;
13403
+ }
13404
+
13405
+ /** Get how far this tween has progressed, from 0 (just started) to 1
13406
+ * (completed). Clamped — overshoot past completion still reads 1.
13407
+ * @returns {number}
13408
+ * @memberof TweenSystem */
13409
+ getPercent()
13410
+ {
13411
+ return percent(this.duration - this.life, 0, this.duration);
13412
+ }
13413
+
13414
+ /** Get the current interpolated value (the value most recently passed to
13415
+ * the callback). Returns a number, Vector2, or Color depending on the
13416
+ * tween's start/end types.
13417
+ * @returns {number|Vector2|Color}
13418
+ * @memberof TweenSystem */
13419
+ getValue()
13420
+ {
13421
+ return this.interp(this.life);
13422
+ }
13423
+
13424
+ /** Compute the interpolated value at the given remaining `life`.
13425
+ * At life === duration the result is `start`; at life === 0 it is `end`.
13426
+ * @param {number} life
13427
+ * @returns {number}
13428
+ * @memberof TweenSystem */
13429
+ interp(life)
13430
+ {
13431
+ const x = this.ease((this.duration - life) / this.duration);
13432
+ if (isLerpable(this.start))
13433
+ return this.start.lerp(this.end, x);
13434
+ return this.start + (this.end - this.start) * x;
13435
+ }
13436
+
13437
+ /** Remove this tween from the active list and prevent any pending then-callback.
13438
+ * @memberof TweenSystem */
13439
+ stop()
13440
+ {
13441
+ const i = tweenActive.indexOf(this);
13442
+ if (i >= 0) tweenActive.splice(i, 1);
13443
+ this.thenCallback = undefined;
13444
+ }
13445
+ }
13446
+
13447
+ /** Library of named easing curves and direction modifiers.
13448
+ * All curves accept `x` in [0,1] and return [0,1] (with possible overshoot
13449
+ * for ELASTIC/BACK/SPRING/BOUNCE). Curves are values you pass to `setEase`
13450
+ * or compose via the IN/OUT/IN_OUT/PIECEWISE/BEZIER modifiers.
13451
+ * @memberof TweenSystem
13452
+ * @example
13453
+ * // Use a basic curve
13454
+ * new Tween(callback, 0, 10, 1).setEase(Ease.SINE);
13455
+ * // Use a modifier on a curve
13456
+ * new Tween(callback, 0, 10, 1).setEase(Ease.OUT(Ease.BACK));
13457
+ */
13458
+ const Ease =
13459
+ {
13460
+ /** Linear (identity) curve.
13461
+ * @param {number} x
13462
+ * @returns {number}
13463
+ * @memberof TweenSystem */
13464
+ LINEAR: (x) => x,
13465
+
13466
+ /** Power curve factory: `Ease.POWER(n)` returns `x => x**n`.
13467
+ * Use n=2 for quadratic, n=3 for cubic, etc.
13468
+ * @param {number} n
13469
+ * @returns {function(number):number}
13470
+ * @memberof TweenSystem */
13471
+ POWER: (n) => (x) => x ** n,
13472
+
13473
+ /** Sine ease-in curve: starts slow, ends fast.
13474
+ * @param {number} x
13475
+ * @returns {number}
13476
+ * @memberof TweenSystem */
13477
+ SINE: (x) => 1 - Math.cos(x * (Math.PI / 2)),
13478
+
13479
+ /** Circular ease-in curve.
13480
+ * @param {number} x
13481
+ * @returns {number}
13482
+ * @memberof TweenSystem */
13483
+ CIRC: (x) => 1 - Math.sqrt(1 - x * x),
13484
+
13485
+ /** Exponential ease-in curve (`2^(10x-10)`).
13486
+ * @param {number} x
13487
+ * @returns {number}
13488
+ * @memberof TweenSystem */
13489
+ EXPO: (x) => 2 ** (10 * x - 10),
13490
+
13491
+ /** Back ease-in: overshoots backward at the start before snapping forward.
13492
+ * @param {number} x
13493
+ * @returns {number}
13494
+ * @memberof TweenSystem */
13495
+ BACK: (x) => x * x * (2.70158 * x - 1.70158),
13496
+
13497
+ /** Elastic ease-in: oscillates with decreasing amplitude.
13498
+ * @param {number} x
13499
+ * @returns {number}
13500
+ * @memberof TweenSystem */
13501
+ ELASTIC: (x) =>
13502
+ -(2 ** (10 * x - 10)) * Math.sin(((37 - 40 * x) * Math.PI) / 6),
13503
+
13504
+ /** Spring-like ease-out: oscillates outward after passing the target.
13505
+ * @param {number} x
13506
+ * @returns {number}
13507
+ * @memberof TweenSystem */
13508
+ SPRING: (x) =>
13509
+ 1 -
13510
+ (Math.sin(Math.PI * (1 - x) * (0.2 + 2.5 * (1 - x) ** 3)) *
13511
+ Math.pow(x, 2.2) +
13512
+ (1 - x)) *
13513
+ (1.0 + 1.2 * x),
13514
+
13515
+ /** Bouncing ease-in: slow ramp with bouncing impacts near the end.
13516
+ * Symmetric with the other base curves, which are all ease-in. To get the
13517
+ * classic "object falls and hits the ground" shape (bounces near x=1),
13518
+ * wrap with `Ease.OUT`: `Ease.OUT(Ease.BOUNCE)`.
13519
+ * @param {number} x
13520
+ * @returns {number}
13521
+ * @memberof TweenSystem
13522
+ * @example
13523
+ * Ease.BOUNCE // ease-in bounce (slow, then bouncy at end)
13524
+ * Ease.OUT(Ease.BOUNCE) // ease-out bounce (object hits ground)
13525
+ * Ease.IN_OUT(Ease.BOUNCE) // bounces at both ends
13526
+ */
13527
+ BOUNCE: (x) =>
13528
+ {
13529
+ // Inverted form of the standard easeOutBounce: 1 - bounceOut(1 - x).
13530
+ let t = 1 - x, f;
13531
+ if (t < 4 / 11) f = 7.5625 * t * t;
13532
+ else if (t < 8 / 11) f = 7.5625 * (t -= 6 / 11) * t + 0.75;
13533
+ else if (t < 10 / 11) f = 7.5625 * (t -= 9 / 11) * t + 0.9375;
13534
+ else f = 7.5625 * (t -= 10.5 / 11) * t + 0.984375;
13535
+ return 1 - f;
13536
+ },
13537
+
13538
+ /** Ease-in direction modifier: returns the curve unchanged. Symmetric
13539
+ * with `OUT` and `IN_OUT`. Base curves are already ease-in by
13540
+ * convention, so wrapping a curve in `IN` is a no-op — useful when
13541
+ * picking the direction programmatically.
13542
+ * @param {function(number):number} f - Curve to use as ease-in (returned unchanged)
13543
+ * @returns {function(number):number}
13544
+ * @memberof TweenSystem
13545
+ * @example
13546
+ * // Pick direction at runtime
13547
+ * const dir = bouncyMode ? Ease.OUT : Ease.IN;
13548
+ * new Tween(cb, 0, 10, 1).setEase(dir(Ease.BACK));
13549
+ */
13550
+ IN: (f) => f,
13551
+
13552
+ /** Reverse a curve so it eases out instead of in: `x => 1 - f(1 - x)`.
13553
+ * @param {function(number):number} f
13554
+ * @returns {function(number):number}
13555
+ * @memberof TweenSystem
13556
+ * @example
13557
+ * Ease.OUT(Ease.POWER(2)) // ease-out quadratic
13558
+ */
13559
+ OUT: (f) => (x) => 1 - f(1 - x),
13560
+
13561
+ /** Combine the first half of `f` with `Ease.OUT(f)` for a symmetric curve.
13562
+ * Bug-fix vs the original library: the original referenced an undefined
13563
+ * global `Piecewise`; this implementation routes through `Ease.PIECEWISE`.
13564
+ * @param {function(number):number} f
13565
+ * @returns {function(number):number}
13566
+ * @memberof TweenSystem */
13567
+ IN_OUT: (f) => Ease.PIECEWISE(f, Ease.OUT(f)),
13568
+
13569
+ /** Split [0,1] into N equal sections and run a different curve in each.
13570
+ * Each curve is mapped to its section: section i runs over [i/n, (i+1)/n]
13571
+ * and its output is mapped to [i/n, (i+1)/n] of the overall range.
13572
+ * @param {...function(number):number} fns
13573
+ * @returns {function(number):number}
13574
+ * @memberof TweenSystem */
13575
+ PIECEWISE: (...fns) =>
13576
+ {
13577
+ const n = fns.length;
13578
+ return (x) =>
13579
+ {
13580
+ const i = (x * n - 1e-9) >> 0;
13581
+ return (fns[i]((x - i / n) * n) + i) / n;
13582
+ };
13583
+ },
13584
+
13585
+ /** Cubic Bezier curve solver in the style of CSS `cubic-bezier`.
13586
+ * Control points (0,0), (x1,y1), (x2,y2), (1,1).
13587
+ * @param {number} x1
13588
+ * @param {number} y1
13589
+ * @param {number} x2
13590
+ * @param {number} y2
13591
+ * @returns {function(number):number}
13592
+ * @memberof TweenSystem
13593
+ * @example
13594
+ * Ease.BEZIER(0.25, 0.1, 0.25, 1) // CSS "ease"
13595
+ */
13596
+ BEZIER: (x1, y1, x2, y2) =>
13597
+ {
13598
+ // Parametric cubic Bezier with implicit (0,0) and (1,1) endpoints.
13599
+ const curve = (t) =>
13600
+ {
13601
+ const u = 1 - t;
13602
+ const c1 = 3 * u * u * t;
13603
+ const c2 = 3 * u * t * t;
13604
+ const t3 = t ** 3;
13605
+ return [c1 * x1 + c2 * x2 + t3, c1 * y1 + c2 * y2 + t3];
13606
+ };
13607
+ return (x) =>
13608
+ {
13609
+ // Binary search for t such that curve(t).x ≈ x, then return curve(t).y.
13610
+ let t0 = 0, t1 = 1;
13611
+ for (let i = 0; i < 128; i++)
13612
+ {
13613
+ const tMid = (t0 + t1) / 2;
13614
+ const [bx, by] = curve(tMid);
13615
+ if (Math.abs(bx - x) < 1e-5) return by;
13616
+ if (bx < x) t0 = tMid; else t1 = tMid;
13617
+ }
13618
+ return curve((t0 + t1) / 2)[1];
13619
+ };
13620
+ },
13621
+ };
13622
+
13623
+ /** Tween a property on an object by dot-path. Returns the underlying Tween
13624
+ * so all chaining methods (`setEase`, `then`, `loop`, `pingPong`, etc.)
13625
+ * remain available.
13626
+ *
13627
+ * `start` and `end` may be numbers, Vector2 instances, Color instances, or
13628
+ * any object with a `lerp(other, percent) => sameType` method.
13629
+ * @param {Object} target - The object whose property is being animated
13630
+ * @param {string} propertyPath - Dot-separated path, e.g. `'pos.x'` or `'color'`
13631
+ * @param {number|Vector2|Color} start - Starting value
13632
+ * @param {number|Vector2|Color} end - Ending value
13633
+ * @param {number} [duration=1] - Duration in seconds
13634
+ * @param {Object} [options] - Same options as the Tween constructor
13635
+ * @returns {Tween}
13636
+ * @memberof TweenSystem
13637
+ * @example
13638
+ * // Numeric: slide an object's x with an ease-out sine curve
13639
+ * tweenProperty(player, 'pos.x', 0, 10, 2).setEase(Ease.OUT(Ease.SINE));
13640
+ * // Vector2: animate a position diagonally
13641
+ * tweenProperty(player, 'pos', vec2(-5, 0), vec2(5, 3), 2);
13642
+ * // Color: pulse between two colors
13643
+ * tweenProperty(sprite, 'color', RED, BLUE, 1).pingPong();
13644
+ */
13645
+ function tweenProperty(target, propertyPath, start, end, duration = 1, options = {})
13646
+ {
13647
+ ASSERT(target != null && typeof target === 'object', 'tweenProperty target must be an object');
13648
+ ASSERT(isString(propertyPath) && propertyPath.length > 0, 'tweenProperty propertyPath must be a non-empty string');
13649
+
13650
+ const parts = propertyPath.split('.');
13651
+ const lastKey = parts.pop();
13652
+ const callback = (value) =>
13653
+ {
13654
+ let obj = target;
13655
+ for (const k of parts) obj = obj[k];
13656
+ obj[lastKey] = value;
13657
+ };
13658
+ return new Tween(callback, start, end, duration, options);
13659
+ }
13660
+
13661
+ // Continuation that schedules the next loop iteration when one finishes.
13662
+ // Called from the completed tween's `then` slot. Decrements the counter and
13663
+ // only spawns a new tween if more iterations remain.
13664
+ function loopContinuation(prev)
13665
+ {
13666
+ if (prev.loopRemaining !== Infinity && prev.loopRemaining <= 1) return;
13667
+ const next = new Tween(prev.callback, prev.start, prev.end, prev.duration,
13668
+ { ease: prev.ease, useRealTime: prev.useRealTime });
13669
+ next.loopRemaining = prev.loopRemaining === Infinity
13670
+ ? Infinity
13671
+ : prev.loopRemaining - 1;
13672
+ next.thenCallback = () => loopContinuation(next);
13673
+ }
13674
+
13675
+ // Continuation for pingPong: spawns a new tween with start and end swapped.
13676
+ function pingPongContinuation(prev)
13677
+ {
13678
+ if (prev.loopRemaining !== Infinity && prev.loopRemaining <= 1) return;
13679
+ const next = new Tween(prev.callback, prev.end, prev.start, prev.duration,
13680
+ { ease: prev.ease, useRealTime: prev.useRealTime });
13681
+ next.loopRemaining = prev.loopRemaining === Infinity
13682
+ ? Infinity
13683
+ : prev.loopRemaining - 1;
13684
+ next.thenCallback = () => pingPongContinuation(next);
13685
+ }
13686
+
13687
+ /** Engine plugin hook: advance every active tween by the appropriate delta.
13688
+ * Called once per render frame by the engine (no arguments). May also be
13689
+ * called explicitly with `(gameDelta, realDelta)` to drive tweens manually
13690
+ * — useful for headless tests or custom replay/scrubbing systems.
13691
+ * @param {number} [gameDelta] - Game-time delta in seconds; default: time - lastTime
13692
+ * @param {number} [realDelta] - Real-time delta in seconds; default: timeReal - lastTimeReal
13693
+ * @memberof TweenSystem */
13694
+ function tweenUpdate(gameDelta, realDelta)
13695
+ {
13696
+ if (gameDelta === undefined)
13697
+ {
13698
+ // Engine path: compute deltas from engine time globals.
13699
+ gameDelta = time - lastTime;
13700
+ realDelta = timeReal - lastTimeReal;
13701
+ lastTime = time;
13702
+ lastTimeReal = timeReal;
13703
+ }
13704
+ else if (realDelta === undefined)
13705
+ {
13706
+ // Manual path with one arg: real and game advance together.
13707
+ realDelta = gameDelta;
13708
+ }
13709
+
13710
+ // Iterate in reverse so removals don't disturb iteration.
13711
+ for (let i = tweenActive.length; i--;)
13712
+ {
13713
+ const t = tweenActive[i];
13714
+ if (t.paused) continue;
13715
+ const dt = t.useRealTime ? realDelta : gameDelta;
13716
+ if (dt <= 0) continue;
13717
+
13718
+ t.life -= dt;
13719
+ if (t.life > 0)
13720
+ {
13721
+ t.callback(t.interp(t.life));
13722
+ }
13723
+ else
13724
+ {
13725
+ // Completion: fire end value, remove from active, fire then-callback.
13726
+ t.callback(t.interp(0));
13727
+ tweenActive.splice(i, 1);
13728
+ const cb = t.thenCallback;
13729
+ t.thenCallback = undefined;
13730
+ if (cb) cb();
13731
+ }
13732
+ }
13733
+ }
13734
+
13735
+ /** Stop every active tween and clear their then-callbacks. Useful for resets
13736
+ * on level transitions or when changing scenes.
13737
+ * @memberof TweenSystem */
13738
+ function tweenStopAll()
13739
+ {
13740
+ for (const t of tweenActive) t.thenCallback = undefined;
13741
+ tweenActive.length = 0;
13742
+ }
13743
+
13744
+ // Register with the engine so tweens auto-advance.
13745
+ engineAddPlugin(tweenUpdate);
13746
+
13045
13747
 
13046
13748
  /**
13047
13749
  * LittleJS Module Export
@@ -13096,6 +13798,7 @@ export
13096
13798
  cameraPos,
13097
13799
  cameraAngle,
13098
13800
  cameraScale,
13801
+ timeScale,
13099
13802
  canvasColorTiles,
13100
13803
  canvasClearColor,
13101
13804
  canvasMaxSize,
@@ -13125,7 +13828,7 @@ export
13125
13828
  gamepadDirectionEmulateStick,
13126
13829
  inputWASDEmulateDirection,
13127
13830
  touchGamepadEnable,
13128
- touchGamepadCenterButton,
13831
+ touchGamepadCenterButtonSize,
13129
13832
  touchGamepadAnalog,
13130
13833
  touchGamepadSize,
13131
13834
  touchGamepadAlpha,
@@ -13143,6 +13846,7 @@ export
13143
13846
  setCameraPos,
13144
13847
  setCameraAngle,
13145
13848
  setCameraScale,
13849
+ setTimeScale,
13146
13850
  setCanvasColorTiles,
13147
13851
  setCanvasClearColor,
13148
13852
  setCanvasMaxSize,
@@ -13173,7 +13877,7 @@ export
13173
13877
  setGamepadDirectionEmulateStick,
13174
13878
  setInputWASDEmulateDirection,
13175
13879
  setTouchGamepadEnable,
13176
- setTouchGamepadCenterButton,
13880
+ setTouchGamepadCenterButtonSize,
13177
13881
  setTouchGamepadButtonCount,
13178
13882
  setTouchGamepadAnalog,
13179
13883
  setTouchGamepadSize,
@@ -13213,10 +13917,12 @@ export
13213
13917
  distanceAngle,
13214
13918
  lerpAngle,
13215
13919
  lerp,
13920
+ percentLerp,
13216
13921
  smoothStep,
13217
13922
  nearestPowerOfTwo,
13218
13923
  isOverlapping,
13219
13924
  isIntersecting,
13925
+ lineTest,
13220
13926
  oscillate,
13221
13927
 
13222
13928
  // Utilities
@@ -13289,6 +13995,7 @@ export
13289
13995
  drawTile,
13290
13996
  drawRect,
13291
13997
  drawRectGradient,
13998
+ drawTextureWrapped,
13292
13999
  drawLineList,
13293
14000
  drawLine,
13294
14001
  drawPoly,
@@ -13355,6 +14062,8 @@ export
13355
14062
  gamepadStick,
13356
14063
  gamepadDpad,
13357
14064
  gamepadConnected,
14065
+ gamepadVibrate,
14066
+ gamepadVibrateStop,
13358
14067
  vibrate,
13359
14068
  vibrateStop,
13360
14069
  pointerLockRequest,
@@ -13460,4 +14169,11 @@ export
13460
14169
  drawNineSliceScreen,
13461
14170
  drawThreeSlice,
13462
14171
  drawThreeSliceScreen,
14172
+
14173
+ // Tween System
14174
+ Tween,
14175
+ tweenProperty,
14176
+ tweenStopAll,
14177
+ tweenUpdate,
14178
+ Ease,
13463
14179
  }