littlejsengine 1.18.19 → 1.18.22
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 +15 -1
- package/dist/littlejs.d.ts +215 -20
- package/dist/littlejs.esm.js +709 -58
- package/dist/littlejs.esm.min.js +1 -1
- package/dist/littlejs.js +690 -57
- package/dist/littlejs.min.js +1 -1
- package/dist/littlejs.release.js +687 -57
- package/package.json +1 -1
- package/plugins/box2d.js +12 -11
- package/plugins/newgrounds.js +2 -1
- package/plugins/pluginExport.js +18 -1
- package/plugins/postProcess.js +5 -4
- package/plugins/textureSheet.js +439 -0
- package/plugins/threejs.js +155 -0
- package/plugins/tweenSystem.js +5 -1
- package/plugins/uiSystem.js +4 -3
- package/src/engine.js +6 -2
- package/src/engineAudio.js +2 -2
- package/src/engineBuild.mjs +4 -2
- package/src/engineDebug.js +3 -0
- package/src/engineDraw.js +32 -14
- package/src/engineInput.js +8 -4
- package/src/engineMath.js +2 -2
- package/src/engineParticles.js +5 -6
- package/src/engineSettings.js +1 -0
- package/src/engineTileLayer.js +10 -6
- package/src/engineUtilities.js +1 -1
package/dist/littlejs.release.js
CHANGED
|
@@ -35,7 +35,7 @@ const engineName = 'LittleJS';
|
|
|
35
35
|
* @type {string}
|
|
36
36
|
* @default
|
|
37
37
|
* @memberof Engine */
|
|
38
|
-
const engineVersion = '1.18.
|
|
38
|
+
const engineVersion = '1.18.22';
|
|
39
39
|
|
|
40
40
|
/** Frames per second to update
|
|
41
41
|
* @type {number}
|
|
@@ -474,6 +474,11 @@ function engineObjectsUpdate()
|
|
|
474
474
|
// get list of solid objects for physics optimization
|
|
475
475
|
engineObjectsCollide = engineObjects.filter(o=>o.collideSolidObjects);
|
|
476
476
|
|
|
477
|
+
// update physics before object update
|
|
478
|
+
for (const o of engineObjects)
|
|
479
|
+
if (!o.parent && !o.destroyed)
|
|
480
|
+
o.updatePhysics();
|
|
481
|
+
|
|
477
482
|
// recursive object update
|
|
478
483
|
function updateChildObject(o)
|
|
479
484
|
{
|
|
@@ -489,7 +494,6 @@ function engineObjectsUpdate()
|
|
|
489
494
|
|
|
490
495
|
// update top level objects
|
|
491
496
|
o.update();
|
|
492
|
-
o.updatePhysics();
|
|
493
497
|
for (const child of o.children)
|
|
494
498
|
updateChildObject(child);
|
|
495
499
|
o.updateTransforms();
|
|
@@ -815,7 +819,7 @@ function smoothStep(percent) { return percent * percent * (3 - 2 * percent); }
|
|
|
815
819
|
* @param {number} value
|
|
816
820
|
* @return {boolean}
|
|
817
821
|
* @memberof Math */
|
|
818
|
-
function isPowerOfTwo(value) { return !(value & (value - 1)); }
|
|
822
|
+
function isPowerOfTwo(value) { return value > 0 && !(value & (value - 1)); }
|
|
819
823
|
|
|
820
824
|
/** Returns the nearest power of two not less than the value
|
|
821
825
|
* @param {number} value
|
|
@@ -892,7 +896,7 @@ function isIntersecting(start, end, pos, size)
|
|
|
892
896
|
* @memberof Math */
|
|
893
897
|
function oscillate(frequency=1, amplitude=1, t=time, offset=0, type=0)
|
|
894
898
|
{
|
|
895
|
-
const phase = (offset + t*frequency
|
|
899
|
+
const phase = mod(offset + t*frequency, 1);
|
|
896
900
|
let value;
|
|
897
901
|
|
|
898
902
|
if (type === 1) // triangle
|
|
@@ -1828,7 +1832,7 @@ const MAGENTA = debugProtectConstant(rgb(1,0,1));
|
|
|
1828
1832
|
class Timer
|
|
1829
1833
|
{
|
|
1830
1834
|
/** Create a timer object set time passed in
|
|
1831
|
-
* @param {number} [timeLeft] - How much time left before the timer
|
|
1835
|
+
* @param {number} [timeLeft] - How much time left before the timer is elapsed in seconds (undefined = unset)
|
|
1832
1836
|
* @param {boolean} [useRealTime] - Should the timer keep running even when the game is paused? (useful for UI) */
|
|
1833
1837
|
constructor(timeLeft, useRealTime=false)
|
|
1834
1838
|
{
|
|
@@ -2427,6 +2431,7 @@ let vibrateEnable = true;
|
|
|
2427
2431
|
let soundEnable = true;
|
|
2428
2432
|
|
|
2429
2433
|
/** Volume scale to apply to all sound, music and speech
|
|
2434
|
+
* Use setSoundVolume to also update the audio master gain immediately
|
|
2430
2435
|
* @type {number}
|
|
2431
2436
|
* @default
|
|
2432
2437
|
* @memberof Settings */
|
|
@@ -3471,8 +3476,9 @@ class TileInfo
|
|
|
3471
3476
|
* @param {TextureInfo} [textureInfo] - Texture info to use
|
|
3472
3477
|
* @param {number} [padding] - How many pixels padding around all sides of each tile (increases grid size, does not affect tile size)
|
|
3473
3478
|
* @param {number} [bleed] - How many pixels smaller to shrink UVS of tiles (does not affect grid size, only UVs)
|
|
3479
|
+
* @param {number} [columns] - How many frames per row for frame(), 0 to keep frames on a single row
|
|
3474
3480
|
*/
|
|
3475
|
-
constructor(pos=vec2(), size=tileDefaultSize, textureInfo=textureInfos[0], padding=tileDefaultPadding, bleed=tileDefaultBleed)
|
|
3481
|
+
constructor(pos=vec2(), size=tileDefaultSize, textureInfo=textureInfos[0], padding=tileDefaultPadding, bleed=tileDefaultBleed, columns=0)
|
|
3476
3482
|
{
|
|
3477
3483
|
/** @property {Vector2} - Top left corner of tile in pixels */
|
|
3478
3484
|
this.pos = pos.copy();
|
|
@@ -3484,6 +3490,8 @@ class TileInfo
|
|
|
3484
3490
|
this.textureInfo = textureInfo;
|
|
3485
3491
|
/** @property {number} - Shrinks tile by this many pixels to prevent neighbors bleeding */
|
|
3486
3492
|
this.bleed = bleed;
|
|
3493
|
+
/** @property {number} - How many frames per row for frame(), 0 to keep frames on a single row */
|
|
3494
|
+
this.columns = columns;
|
|
3487
3495
|
}
|
|
3488
3496
|
|
|
3489
3497
|
/** Returns a copy of this tile offset by a vector
|
|
@@ -3491,9 +3499,10 @@ class TileInfo
|
|
|
3491
3499
|
* @return {TileInfo}
|
|
3492
3500
|
*/
|
|
3493
3501
|
offset(offset)
|
|
3494
|
-
{ return new TileInfo(this.pos.add(offset), this.size, this.textureInfo, this.padding, this.bleed); }
|
|
3502
|
+
{ return new TileInfo(this.pos.add(offset), this.size, this.textureInfo, this.padding, this.bleed, this.columns); }
|
|
3495
3503
|
|
|
3496
3504
|
/** Returns a copy of this tile offset by a number of animation frames
|
|
3505
|
+
* Frames wrap down to the next row if columns is set
|
|
3497
3506
|
* @param {number} frame - Offset to apply in animation frames
|
|
3498
3507
|
* @return {TileInfo}
|
|
3499
3508
|
*/
|
|
@@ -3501,11 +3510,33 @@ class TileInfo
|
|
|
3501
3510
|
{
|
|
3502
3511
|
ASSERT(typeof frame === 'number');
|
|
3503
3512
|
const w = this.size.x + this.padding*2;
|
|
3504
|
-
const
|
|
3505
|
-
|
|
3506
|
-
|
|
3513
|
+
const h = this.size.y + this.padding*2;
|
|
3514
|
+
const x = (this.columns ? frame % this.columns : frame) * w;
|
|
3515
|
+
const y = (this.columns ? frame / this.columns | 0 : 0) * h;
|
|
3516
|
+
ASSERT(this.pos.x + x + this.size.x <= this.textureInfo.size.x, 'frame extends beyond texture width!');
|
|
3517
|
+
ASSERT(this.pos.y + y + this.size.y <= this.textureInfo.size.y, 'frame extends beyond texture height!');
|
|
3518
|
+
return this.offset(new Vector2(x, y));
|
|
3519
|
+
}
|
|
3520
|
+
|
|
3521
|
+
/** Set how many frames per row this tile uses, so frame() can wrap
|
|
3522
|
+
* @param {number} [columns] - Frames per row, 0 to keep frames on a single row
|
|
3523
|
+
* @return {TileInfo}
|
|
3524
|
+
*/
|
|
3525
|
+
setColumns(columns=0)
|
|
3526
|
+
{
|
|
3527
|
+
ASSERT(isNumber(columns) && columns >= 0, 'columns must be a number >= 0');
|
|
3528
|
+
this.columns = columns;
|
|
3529
|
+
return this;
|
|
3507
3530
|
}
|
|
3508
3531
|
|
|
3532
|
+
/**
|
|
3533
|
+
* Returns a tile info for an index using this tile as reference
|
|
3534
|
+
* @param {Vector2|number} [index=0]
|
|
3535
|
+
* @return {TileInfo}
|
|
3536
|
+
*/
|
|
3537
|
+
index(index)
|
|
3538
|
+
{ return tile(index, this.size, this.textureInfo, this.padding, this.bleed).setColumns(this.columns); }
|
|
3539
|
+
|
|
3509
3540
|
/**
|
|
3510
3541
|
* Set this tile to use a full image in a texture info
|
|
3511
3542
|
* @param {TextureInfo} [textureInfo]
|
|
@@ -3516,17 +3547,9 @@ class TileInfo
|
|
|
3516
3547
|
this.textureInfo = textureInfo;
|
|
3517
3548
|
this.pos = new Vector2;
|
|
3518
3549
|
this.size = textureInfo.size.copy();
|
|
3519
|
-
this.bleed = this.padding = 0;
|
|
3550
|
+
this.bleed = this.padding = this.columns = 0;
|
|
3520
3551
|
return this;
|
|
3521
3552
|
}
|
|
3522
|
-
|
|
3523
|
-
/**
|
|
3524
|
-
* Returns a tile info for an index using this tile as reference
|
|
3525
|
-
* @param {Vector2|number} [index=0]
|
|
3526
|
-
* @return {TileInfo}
|
|
3527
|
-
*/
|
|
3528
|
-
tile(index)
|
|
3529
|
-
{ return tile(index, this.size, this.textureInfo, this.padding, this.bleed); }
|
|
3530
3553
|
}
|
|
3531
3554
|
|
|
3532
3555
|
/**
|
|
@@ -5224,7 +5247,10 @@ function inputInit()
|
|
|
5224
5247
|
{
|
|
5225
5248
|
inputData[0][e.code] = (inputData[0][e.code]&2) | 4;
|
|
5226
5249
|
if (inputWASDEmulateDirection)
|
|
5227
|
-
|
|
5250
|
+
{
|
|
5251
|
+
const remap = remapKey(e.code);
|
|
5252
|
+
inputData[0][remap] = (inputData[0][remap]&2) | 4;
|
|
5253
|
+
}
|
|
5228
5254
|
}
|
|
5229
5255
|
function remapKey(k)
|
|
5230
5256
|
{
|
|
@@ -5299,7 +5325,7 @@ function inputInit()
|
|
|
5299
5325
|
document.addEventListener('touchend', (e)=> handleTouch(e), { passive: false });
|
|
5300
5326
|
|
|
5301
5327
|
// handle all touch events the same way
|
|
5302
|
-
let wasTouching;
|
|
5328
|
+
let wasTouching, touchIdentifier;
|
|
5303
5329
|
function handleTouch(e)
|
|
5304
5330
|
{
|
|
5305
5331
|
if (!touchInputEnable) return;
|
|
@@ -5330,10 +5356,11 @@ function inputInit()
|
|
|
5330
5356
|
const pos = vec2(gameTouches[0].clientX, gameTouches[0].clientY);
|
|
5331
5357
|
const mousePosScreenLast = mousePosScreen;
|
|
5332
5358
|
mousePosScreen = mouseEventToScreen(pos);
|
|
5333
|
-
if (wasTouching)
|
|
5359
|
+
if (wasTouching && gameTouches[0].identifier === touchIdentifier)
|
|
5334
5360
|
mouseDeltaScreen = mouseDeltaScreen.add(mousePosScreen.subtract(mousePosScreenLast));
|
|
5335
|
-
else
|
|
5361
|
+
else if (!wasTouching)
|
|
5336
5362
|
inputData[0][button] = 3;
|
|
5363
|
+
touchIdentifier = gameTouches[0].identifier;
|
|
5337
5364
|
}
|
|
5338
5365
|
else if (wasTouching)
|
|
5339
5366
|
inputData[0][button] = inputData[0][button] & 2 | 4;
|
|
@@ -6491,7 +6518,7 @@ class SoundInstance
|
|
|
6491
6518
|
* @param {number} [rate] - How quickly to speak
|
|
6492
6519
|
* @param {number} [pitch] - How much to change the pitch by
|
|
6493
6520
|
* @param {string} [language] - The language/accent to use (examples: en, it, ru, ja, zh)
|
|
6494
|
-
* @return {SpeechSynthesisUtterance} - The utterance that was spoken
|
|
6521
|
+
* @return {SpeechSynthesisUtterance|undefined} - The utterance that was spoken, or undefined if speech is unavailable
|
|
6495
6522
|
* @memberof Audio */
|
|
6496
6523
|
function speak(text, volume=1, rate=1, pitch=1, language='')
|
|
6497
6524
|
{
|
|
@@ -6544,7 +6571,7 @@ function getNoteFrequency(semitoneOffset, rootFrequency=220)
|
|
|
6544
6571
|
* @param {number} [pan] - How much to apply stereo panning
|
|
6545
6572
|
* @param {boolean} [loop] - True if the sound should loop when it reaches the end
|
|
6546
6573
|
* @param {number} [sampleRate=44100] - Sample rate for the sound
|
|
6547
|
-
* @param {GainNode} [gainNode] - Optional gain node for volume control while playing
|
|
6574
|
+
* @param {GainNode} [gainNode] - Optional gain node for volume control while playing (disconnected when the sound ends)
|
|
6548
6575
|
* @param {number} [offset] - Offset in seconds to start playback from
|
|
6549
6576
|
* @param {AudioEndedCallback} [onended] - Callback for when the sound ends
|
|
6550
6577
|
* @return {AudioBufferSourceNode} - The source node of the sound played, may be undefined if play fails
|
|
@@ -6786,10 +6813,14 @@ function tileCollisionGetData(pos, solidOnly=true)
|
|
|
6786
6813
|
// check all tile collision layers
|
|
6787
6814
|
for (const layer of tileCollisionLayers)
|
|
6788
6815
|
if (!solidOnly || layer.isSolid)
|
|
6789
|
-
if (pos.arrayCheck(layer.size))
|
|
6790
6816
|
{
|
|
6791
|
-
|
|
6792
|
-
|
|
6817
|
+
// convert world pos to layer local space
|
|
6818
|
+
const layerPos = pos.subtract(layer.pos);
|
|
6819
|
+
if (layerPos.arrayCheck(layer.size))
|
|
6820
|
+
{
|
|
6821
|
+
const data = layer.getCollisionData(layerPos);
|
|
6822
|
+
if (data) return data;
|
|
6823
|
+
}
|
|
6793
6824
|
}
|
|
6794
6825
|
return 0;
|
|
6795
6826
|
}
|
|
@@ -7064,7 +7095,7 @@ class TileLayer extends CanvasLayer
|
|
|
7064
7095
|
|
|
7065
7096
|
/** @property {TileInfo} - Default tile info for layer */
|
|
7066
7097
|
this.tileInfo = undefined;
|
|
7067
|
-
/** @property {Array<TileLayerData>} -
|
|
7098
|
+
/** @property {Array<TileLayerData>} - Array of tile data for the layer */
|
|
7068
7099
|
this.data = [];
|
|
7069
7100
|
/** @property {boolean} - Is this layer using a webgl texture? */
|
|
7070
7101
|
this.isUsingWebGL = false;
|
|
@@ -7149,7 +7180,7 @@ class TileLayer extends CanvasLayer
|
|
|
7149
7180
|
|
|
7150
7181
|
const size = this.drawSize || this.size;
|
|
7151
7182
|
const pos = this.pos.add(size.scale(.5));
|
|
7152
|
-
this.draw(pos,
|
|
7183
|
+
this.draw(pos, size, this.color, this.angle, this.mirror, this.additiveColor);
|
|
7153
7184
|
}
|
|
7154
7185
|
|
|
7155
7186
|
/** Called after this layer is redrawn, does nothing by default */
|
|
@@ -7238,7 +7269,7 @@ class TileLayer extends CanvasLayer
|
|
|
7238
7269
|
const d = this.getData(layerPos);
|
|
7239
7270
|
if (!d || !d.tile) return;
|
|
7240
7271
|
|
|
7241
|
-
const tileInfo = this.tileInfo && this.tileInfo.
|
|
7272
|
+
const tileInfo = this.tileInfo && this.tileInfo.index(d.tile);
|
|
7242
7273
|
this.drawLayerTile(drawPos, drawSize, tileInfo, d.color, d.direction*PI/2, d.mirror);
|
|
7243
7274
|
}
|
|
7244
7275
|
|
|
@@ -7532,8 +7563,8 @@ class TileCollisionLayer extends TileLayer
|
|
|
7532
7563
|
* rgb(1,1,1,1), rgb(0,0,0,1), // colorStartA, colorStartB
|
|
7533
7564
|
* rgb(1,1,1,0), rgb(0,0,0,0), // colorEndA, colorEndB
|
|
7534
7565
|
* 1, .2, .2, .1, .05, // particleTime, sizeStart, sizeEnd, particleSpeed, particleAngleSpeed
|
|
7535
|
-
* .99, 1, 1, PI, .05, // damping, angleDamping, gravityScale, particleCone, fadeRate
|
|
7536
|
-
* .5, 1 // randomness, collide
|
|
7566
|
+
* .99, 1, 1, PI, .05, // damping, angleDamping, gravityScale, particleCone, fadeRate
|
|
7567
|
+
* .5, 1 // randomness, collide
|
|
7537
7568
|
* );
|
|
7538
7569
|
*/
|
|
7539
7570
|
class ParticleEmitter extends EngineObject
|
|
@@ -7925,13 +7956,12 @@ class Particle
|
|
|
7925
7956
|
const hitLayer = tileCollisionTest(this.pos);
|
|
7926
7957
|
if (!testCollision(oldPos))
|
|
7927
7958
|
{
|
|
7928
|
-
// testCollision already invoked collideCallback with the
|
|
7929
|
-
// correct (this, data, pos) args; no need to re-check here.
|
|
7930
7959
|
// test which side we bounced off (or both if a corner)
|
|
7931
7960
|
const isBlockedX = testCollision(vec2(this.pos.x, oldPos.y));
|
|
7932
7961
|
const isBlockedY = testCollision(vec2(oldPos.x, this.pos.y));
|
|
7933
|
-
|
|
7934
|
-
const
|
|
7962
|
+
// collide callback may hit where the layer test does not, so hitLayer can be undefined
|
|
7963
|
+
const hitRestitution = hitLayer ? max(restitution, hitLayer.restitution) : restitution;
|
|
7964
|
+
const hitFriction = hitLayer ? max(friction, hitLayer.friction) : friction;
|
|
7935
7965
|
if (isBlockedX)
|
|
7936
7966
|
{
|
|
7937
7967
|
// move to previous X position and bounce
|
|
@@ -9580,7 +9610,8 @@ class NewgroundsPlugin
|
|
|
9580
9610
|
return;
|
|
9581
9611
|
}
|
|
9582
9612
|
debugMedals && LOG(xmlHttp.responseText);
|
|
9583
|
-
return xmlHttp.responseText && JSON.parse(xmlHttp.responseText);
|
|
9613
|
+
try { return xmlHttp.responseText && JSON.parse(xmlHttp.responseText); }
|
|
9614
|
+
catch(e) { debugMedals && LOG('newgrounds response is not valid JSON', e); }
|
|
9584
9615
|
}
|
|
9585
9616
|
}
|
|
9586
9617
|
/**
|
|
@@ -9599,8 +9630,8 @@ class NewgroundsPlugin
|
|
|
9599
9630
|
let postProcess;
|
|
9600
9631
|
|
|
9601
9632
|
/////////////////////////////////////////////////////////////////////////
|
|
9602
|
-
/**
|
|
9603
|
-
*
|
|
9633
|
+
/**
|
|
9634
|
+
* Post Process Plugin - Applies a full screen shader to the rendered output
|
|
9604
9635
|
* @memberof PostProcess
|
|
9605
9636
|
*/
|
|
9606
9637
|
class PostProcessPlugin
|
|
@@ -9714,8 +9745,9 @@ class PostProcessPlugin
|
|
|
9714
9745
|
workCanvas.height = mainCanvasSize.y;
|
|
9715
9746
|
glCopyToContext(workContext);
|
|
9716
9747
|
workContext.drawImage(mainCanvas, 0, 0);
|
|
9717
|
-
mainCanvas.width |= 0
|
|
9718
|
-
|
|
9748
|
+
mainCanvas.width |= 0; // setting size clears the main canvas
|
|
9749
|
+
|
|
9750
|
+
|
|
9719
9751
|
// copy work canvas to texture
|
|
9720
9752
|
glContext.texImage2D(glContext.TEXTURE_2D, 0, glContext.RGBA, glContext.RGBA, glContext.UNSIGNED_BYTE, workCanvas);
|
|
9721
9753
|
}
|
|
@@ -10820,7 +10852,7 @@ class UISystemPlugin
|
|
|
10820
10852
|
}
|
|
10821
10853
|
|
|
10822
10854
|
/** Get other axis navigation direction from gamepad or keyboard
|
|
10823
|
-
* @return {
|
|
10855
|
+
* @return {number} */
|
|
10824
10856
|
getNavigationOtherDirection()
|
|
10825
10857
|
{
|
|
10826
10858
|
if (uiSystem.navigationDirection === 2)
|
|
@@ -10909,6 +10941,7 @@ class UISystemPlugin
|
|
|
10909
10941
|
uiSystem.navigationDirection = savedNavigationDirection;
|
|
10910
10942
|
inputClear();
|
|
10911
10943
|
}
|
|
10944
|
+
return confirmMenu;
|
|
10912
10945
|
}
|
|
10913
10946
|
}
|
|
10914
10947
|
|
|
@@ -11342,7 +11375,7 @@ class UITextInput extends UIObject
|
|
|
11342
11375
|
this.onClick();
|
|
11343
11376
|
}
|
|
11344
11377
|
|
|
11345
|
-
/** Stop editing the text
|
|
11378
|
+
/** Stop editing the text */
|
|
11346
11379
|
stopEditing()
|
|
11347
11380
|
{
|
|
11348
11381
|
if (!this.isKeyInputObject())
|
|
@@ -11505,7 +11538,7 @@ class UICheckbox extends UIObject
|
|
|
11505
11538
|
ASSERT(isStringLike(text), 'ui checkbox must be a string');
|
|
11506
11539
|
ASSERT(isColor(color), 'ui checkbox color must be a color');
|
|
11507
11540
|
|
|
11508
|
-
/** @property {boolean} -
|
|
11541
|
+
/** @property {boolean} - Is the checkbox currently checked? */
|
|
11509
11542
|
this.checked = checked;
|
|
11510
11543
|
// set properties
|
|
11511
11544
|
this.text = text;
|
|
@@ -12270,7 +12303,7 @@ class Box2dObject extends EngineObject
|
|
|
12270
12303
|
shape.set_m_vertex3(box2d.vec2dTo(getPoint(i+2)));
|
|
12271
12304
|
const f = this.addShape(shape, density, friction, restitution, isSensor);
|
|
12272
12305
|
fixtures.push(f);
|
|
12273
|
-
|
|
12306
|
+
edgePoints.push(points[i].copy());
|
|
12274
12307
|
}
|
|
12275
12308
|
this.edgeLoops.push(edgePoints);
|
|
12276
12309
|
return fixtures;
|
|
@@ -12336,7 +12369,7 @@ class Box2dObject extends EngineObject
|
|
|
12336
12369
|
/** Sets the position
|
|
12337
12370
|
* @param {Vector2} pos */
|
|
12338
12371
|
setPosition(pos)
|
|
12339
|
-
{ this.setTransform(pos, this.body.GetAngle()); }
|
|
12372
|
+
{ this.setTransform(pos, -this.body.GetAngle()); }
|
|
12340
12373
|
|
|
12341
12374
|
/** Sets the angle
|
|
12342
12375
|
* @param {number} angle */
|
|
@@ -12433,6 +12466,7 @@ class Box2dObject extends EngineObject
|
|
|
12433
12466
|
filter.set_categoryBits(categoryBits);
|
|
12434
12467
|
filter.set_maskBits(0xffff & ~ignoreCategoryBits);
|
|
12435
12468
|
filter.set_groupIndex(groupIndex);
|
|
12469
|
+
fixture.SetFilterData(filter); // applies and refilters contacts
|
|
12436
12470
|
});
|
|
12437
12471
|
}
|
|
12438
12472
|
|
|
@@ -12968,7 +13002,7 @@ class Box2dRevoluteJoint extends Box2dJoint
|
|
|
12968
13002
|
jointDef.set_bodyB(objectB.body);
|
|
12969
13003
|
jointDef.set_localAnchorA(box2d.vec2dTo(localAnchorA));
|
|
12970
13004
|
jointDef.set_localAnchorB(box2d.vec2dTo(localAnchorB));
|
|
12971
|
-
jointDef.set_referenceAngle(
|
|
13005
|
+
jointDef.set_referenceAngle(objectB.body.GetAngle() - objectA.body.GetAngle());
|
|
12972
13006
|
jointDef.set_collideConnected(collide);
|
|
12973
13007
|
super(jointDef);
|
|
12974
13008
|
}
|
|
@@ -13115,14 +13149,14 @@ class Box2dPrismaticJoint extends Box2dJoint
|
|
|
13115
13149
|
anchor ||= box2d.vec2From(objectB.body.GetPosition());
|
|
13116
13150
|
const localAnchorA = objectA.worldToLocal(anchor);
|
|
13117
13151
|
const localAnchorB = objectB.worldToLocal(anchor);
|
|
13118
|
-
const localAxisA =
|
|
13152
|
+
const localAxisA = objectA.worldToLocalVector(worldAxis);
|
|
13119
13153
|
const jointDef = new box2d.instance.b2PrismaticJointDef();
|
|
13120
13154
|
jointDef.set_bodyA(objectA.body);
|
|
13121
13155
|
jointDef.set_bodyB(objectB.body);
|
|
13122
13156
|
jointDef.set_localAnchorA(box2d.vec2dTo(localAnchorA));
|
|
13123
13157
|
jointDef.set_localAnchorB(box2d.vec2dTo(localAnchorB));
|
|
13124
13158
|
jointDef.set_localAxisA(box2d.vec2dTo(localAxisA));
|
|
13125
|
-
jointDef.set_referenceAngle(
|
|
13159
|
+
jointDef.set_referenceAngle(objectB.body.GetAngle() - objectA.body.GetAngle());
|
|
13126
13160
|
jointDef.set_collideConnected(collide);
|
|
13127
13161
|
super(jointDef);
|
|
13128
13162
|
}
|
|
@@ -13225,7 +13259,7 @@ class Box2dWheelJoint extends Box2dJoint
|
|
|
13225
13259
|
anchor ||= box2d.vec2From(objectB.body.GetPosition());
|
|
13226
13260
|
const localAnchorA = objectA.worldToLocal(anchor);
|
|
13227
13261
|
const localAnchorB = objectB.worldToLocal(anchor);
|
|
13228
|
-
const localAxisA =
|
|
13262
|
+
const localAxisA = objectA.worldToLocalVector(worldAxis);
|
|
13229
13263
|
const jointDef = new box2d.instance.b2WheelJointDef();
|
|
13230
13264
|
jointDef.set_bodyA(objectA.body);
|
|
13231
13265
|
jointDef.set_bodyB(objectB.body);
|
|
@@ -13325,7 +13359,7 @@ class Box2dWeldJoint extends Box2dJoint
|
|
|
13325
13359
|
jointDef.set_bodyB(objectB.body);
|
|
13326
13360
|
jointDef.set_localAnchorA(box2d.vec2dTo(localAnchorA));
|
|
13327
13361
|
jointDef.set_localAnchorB(box2d.vec2dTo(localAnchorB));
|
|
13328
|
-
jointDef.set_referenceAngle(
|
|
13362
|
+
jointDef.set_referenceAngle(objectB.body.GetAngle() - objectA.body.GetAngle());
|
|
13329
13363
|
jointDef.set_collideConnected(collide);
|
|
13330
13364
|
super(jointDef);
|
|
13331
13365
|
}
|
|
@@ -13772,7 +13806,7 @@ class Box2dPlugin
|
|
|
13772
13806
|
* @param {number} [lineWidth]
|
|
13773
13807
|
* @param {boolean} [useWebGL=glEnable]
|
|
13774
13808
|
* @param {CanvasRenderingContext2D} [context] */
|
|
13775
|
-
drawFixture(fixture, pos, angle, color=WHITE, lineColor=BLACK, lineWidth=.1,
|
|
13809
|
+
drawFixture(fixture, pos, angle, color=WHITE, lineColor=BLACK, lineWidth=.1, useWebGL, context)
|
|
13776
13810
|
{
|
|
13777
13811
|
const shape = box2d.castShapeObject(fixture.GetShape());
|
|
13778
13812
|
switch (shape.GetType())
|
|
@@ -13782,20 +13816,20 @@ class Box2dPlugin
|
|
|
13782
13816
|
let points = [];
|
|
13783
13817
|
for (let i=shape.GetVertexCount(); i--;)
|
|
13784
13818
|
points.push(box2d.vec2From(shape.GetVertex(i)));
|
|
13785
|
-
drawPoly(points, color, lineWidth, lineColor, pos, angle,
|
|
13819
|
+
drawPoly(points, color, lineWidth, lineColor, pos, angle, useWebGL, false, context);
|
|
13786
13820
|
break;
|
|
13787
13821
|
}
|
|
13788
13822
|
case box2d.instance.b2Shape.e_circle:
|
|
13789
13823
|
{
|
|
13790
13824
|
const radius = shape.get_m_radius();
|
|
13791
|
-
drawCircle(pos, radius*2, color, lineWidth, lineColor,
|
|
13825
|
+
drawCircle(pos, radius*2, color, lineWidth, lineColor, useWebGL, false, context);
|
|
13792
13826
|
break;
|
|
13793
13827
|
}
|
|
13794
13828
|
case box2d.instance.b2Shape.e_edge:
|
|
13795
13829
|
{
|
|
13796
13830
|
const v1 = box2d.vec2From(shape.get_m_vertex1());
|
|
13797
13831
|
const v2 = box2d.vec2From(shape.get_m_vertex2());
|
|
13798
|
-
drawLine(v1, v2, lineWidth, lineColor, pos, angle,
|
|
13832
|
+
drawLine(v1, v2, lineWidth, lineColor, pos, angle, useWebGL, false, context);
|
|
13799
13833
|
break;
|
|
13800
13834
|
}
|
|
13801
13835
|
}
|
|
@@ -14200,6 +14234,444 @@ function getCrescentPoints(pos, size=1, percent=0, angle=0, invert=false, sides=
|
|
|
14200
14234
|
}
|
|
14201
14235
|
return points;
|
|
14202
14236
|
}
|
|
14237
|
+
/**
|
|
14238
|
+
* LittleJS Texture Sheet Plugin
|
|
14239
|
+
* - Packs images into texture sheets as they are loaded
|
|
14240
|
+
* - Sprites are placed automatically, callers get a TileInfo
|
|
14241
|
+
* - Sheets are created and filled as needed
|
|
14242
|
+
* - Sheets fill in call order, images decode in parallel
|
|
14243
|
+
* - Animation frames keep layout and wrap across rows as needed
|
|
14244
|
+
* - WebGL textures upload once per batch of loads
|
|
14245
|
+
* - loadAtlas imports pre-packed atlases (TexturePacker and Aseprite json)
|
|
14246
|
+
* @namespace TextureSheets
|
|
14247
|
+
*/
|
|
14248
|
+
|
|
14249
|
+
/** Width and height in pixels of texture sheets created by loadSprite
|
|
14250
|
+
* @type {number}
|
|
14251
|
+
* @default
|
|
14252
|
+
* @memberof Settings */
|
|
14253
|
+
let textureSheetSize = 2048;
|
|
14254
|
+
|
|
14255
|
+
/** Default padding pixels around each frame packed by loadSprite
|
|
14256
|
+
* @type {number}
|
|
14257
|
+
* @default
|
|
14258
|
+
* @memberof Settings */
|
|
14259
|
+
let textureSheetPadding = 1;
|
|
14260
|
+
|
|
14261
|
+
/** Array of texture sheets created by loadSprite
|
|
14262
|
+
* @type {Array<TextureSheet>}
|
|
14263
|
+
* @memberof TextureSheets */
|
|
14264
|
+
let textureSheets = [];
|
|
14265
|
+
|
|
14266
|
+
// pending loads pack through a queue so sheets fill in call order
|
|
14267
|
+
let textureSheetQueue = Promise.resolve();
|
|
14268
|
+
let textureSheetPendingCount = 0;
|
|
14269
|
+
|
|
14270
|
+
/**
|
|
14271
|
+
* Texture Sheet - A texture that images are packed into as they load
|
|
14272
|
+
* Uses shelf packing, images are placed left to right then wrap to a new row
|
|
14273
|
+
* @memberof TextureSheets
|
|
14274
|
+
*/
|
|
14275
|
+
class TextureSheet
|
|
14276
|
+
{
|
|
14277
|
+
/** Create a texture sheet, called automatically by loadSprite
|
|
14278
|
+
* @param {number} [size] - Width and height of the sheet in pixels */
|
|
14279
|
+
constructor(size=textureSheetSize)
|
|
14280
|
+
{
|
|
14281
|
+
ASSERT(size > 0, 'texture sheet size must be positive');
|
|
14282
|
+
|
|
14283
|
+
/** @property {number} - Width and height of the sheet in pixels */
|
|
14284
|
+
this.size = size;
|
|
14285
|
+
/** @property {OffscreenCanvas} - Canvas holding the packed images */
|
|
14286
|
+
this.canvas = headlessMode ? undefined : new OffscreenCanvas(size, size);
|
|
14287
|
+
/** @property {OffscreenCanvasRenderingContext2D} - 2d context for the canvas */
|
|
14288
|
+
this.context = this.canvas?.getContext('2d');
|
|
14289
|
+
/** @property {TextureInfo} - The texture info for this sheet */
|
|
14290
|
+
this.textureInfo = new TextureInfo(this.canvas);
|
|
14291
|
+
/** @property {Vector2} - Where the next image will be packed */
|
|
14292
|
+
this.cursor = vec2();
|
|
14293
|
+
/** @property {number} - Height of the row being packed */
|
|
14294
|
+
this.rowHeight = 0;
|
|
14295
|
+
/** @property {boolean} - Has the canvas changed since the last webgl upload? */
|
|
14296
|
+
this.glDirty = false;
|
|
14297
|
+
|
|
14298
|
+
if (headlessMode)
|
|
14299
|
+
{
|
|
14300
|
+
// tiles still need bounds when there is no canvas to measure
|
|
14301
|
+
this.textureInfo.size = vec2(size);
|
|
14302
|
+
this.textureInfo.sizeInverse = vec2(1/size);
|
|
14303
|
+
}
|
|
14304
|
+
}
|
|
14305
|
+
|
|
14306
|
+
/** Find a spot for an image on this sheet without drawing it
|
|
14307
|
+
* @param {Vector2} imageSize - Size of the source image in pixels
|
|
14308
|
+
* @param {Vector2} [frameSize] - Size of each frame, or the whole image if not passed
|
|
14309
|
+
* @param {number} [padding] - How many pixels padding around each frame
|
|
14310
|
+
* @return {TileInfo} Tile for the packed image, or undefined if the sheet is full */
|
|
14311
|
+
tryAdd(imageSize, frameSize=imageSize, padding=textureSheetPadding)
|
|
14312
|
+
{
|
|
14313
|
+
ASSERT(isVector2(imageSize) && isVector2(frameSize), 'sizes must be vec2');
|
|
14314
|
+
ASSERT(frameSize.x > 0 && frameSize.y > 0, 'frame size must be positive');
|
|
14315
|
+
ASSERT(imageSize.x % frameSize.x === 0 && imageSize.y % frameSize.y === 0,
|
|
14316
|
+
'image size must be a multiple of the frame size');
|
|
14317
|
+
|
|
14318
|
+
const cellWidth = frameSize.x + padding*2;
|
|
14319
|
+
const cellHeight = frameSize.y + padding*2;
|
|
14320
|
+
const maxColumns = this.size / cellWidth | 0;
|
|
14321
|
+
ASSERT(maxColumns > 0, 'frame is too wide to fit on a texture sheet');
|
|
14322
|
+
|
|
14323
|
+
// keep the layout of the source image, but narrow it if a row is too wide
|
|
14324
|
+
// frames wrap down to the next row, which TileInfo.frame handles via columns
|
|
14325
|
+
const sourceColumns = imageSize.x / frameSize.x;
|
|
14326
|
+
const frameCount = sourceColumns * (imageSize.y / frameSize.y);
|
|
14327
|
+
const columns = min(sourceColumns, maxColumns);
|
|
14328
|
+
const blockWidth = columns * cellWidth;
|
|
14329
|
+
const blockHeight = ceil(frameCount / columns) * cellHeight;
|
|
14330
|
+
|
|
14331
|
+
// probe the placement using locals so a failed try leaves the sheet unchanged
|
|
14332
|
+
let x = this.cursor.x, y = this.cursor.y, rowHeight = this.rowHeight;
|
|
14333
|
+
if (x + blockWidth > this.size)
|
|
14334
|
+
{
|
|
14335
|
+
// start a new row if this one does not have enough space left
|
|
14336
|
+
x = 0;
|
|
14337
|
+
y += rowHeight;
|
|
14338
|
+
rowHeight = 0;
|
|
14339
|
+
}
|
|
14340
|
+
|
|
14341
|
+
// out of space, the caller needs to use a different sheet
|
|
14342
|
+
if (y + blockHeight > this.size)
|
|
14343
|
+
return undefined;
|
|
14344
|
+
|
|
14345
|
+
// commit the placement, tile pos points inside the padding to match how tile() works
|
|
14346
|
+
this.cursor.x = x + blockWidth;
|
|
14347
|
+
this.cursor.y = y;
|
|
14348
|
+
this.rowHeight = max(rowHeight, blockHeight);
|
|
14349
|
+
return new TileInfo(vec2(x + padding, y + padding), frameSize, this.textureInfo, padding, 0, columns);
|
|
14350
|
+
}
|
|
14351
|
+
|
|
14352
|
+
/** Draw an image into this sheet at a tile returned by tryAdd
|
|
14353
|
+
* @param {HTMLImageElement} image - Source image to copy from
|
|
14354
|
+
* @param {TileInfo} tileInfo - Where to put it, from tryAdd
|
|
14355
|
+
* @param {boolean} [update] - Upload to webgl now, pass false when batching */
|
|
14356
|
+
drawImage(image, tileInfo, update=true)
|
|
14357
|
+
{
|
|
14358
|
+
ASSERT(!!this.context, 'texture sheet has no canvas');
|
|
14359
|
+
|
|
14360
|
+
// copy frames in order, reading the source left to right, top to bottom
|
|
14361
|
+
// the destination wraps at tileInfo.columns which may be narrower than the source
|
|
14362
|
+
const frameSize = tileInfo.size;
|
|
14363
|
+
const sourceColumns = image.width / frameSize.x;
|
|
14364
|
+
const frameCount = sourceColumns * (image.height / frameSize.y);
|
|
14365
|
+
const columns = tileInfo.columns || frameCount;
|
|
14366
|
+
const cellWidth = frameSize.x + tileInfo.padding*2;
|
|
14367
|
+
const cellHeight = frameSize.y + tileInfo.padding*2;
|
|
14368
|
+
for (let i = frameCount; i--;)
|
|
14369
|
+
{
|
|
14370
|
+
const sourceX = (i % sourceColumns) * frameSize.x;
|
|
14371
|
+
const sourceY = (i / sourceColumns | 0) * frameSize.y;
|
|
14372
|
+
this.context.drawImage(image,
|
|
14373
|
+
sourceX, sourceY, frameSize.x, frameSize.y,
|
|
14374
|
+
tileInfo.pos.x + (i % columns) * cellWidth,
|
|
14375
|
+
tileInfo.pos.y + (i / columns | 0) * cellHeight,
|
|
14376
|
+
frameSize.x, frameSize.y);
|
|
14377
|
+
}
|
|
14378
|
+
|
|
14379
|
+
// upload now unless the caller is batching more images
|
|
14380
|
+
this.glDirty = true;
|
|
14381
|
+
update && this.updateTexture();
|
|
14382
|
+
}
|
|
14383
|
+
|
|
14384
|
+
/** Upload the canvas to webgl if it has changed since the last upload
|
|
14385
|
+
* Only needed after batching, drawImage uploads automatically by default */
|
|
14386
|
+
updateTexture()
|
|
14387
|
+
{
|
|
14388
|
+
if (!this.glDirty) return;
|
|
14389
|
+
this.glDirty = false;
|
|
14390
|
+
this.textureInfo.createWebGLTexture();
|
|
14391
|
+
}
|
|
14392
|
+
}
|
|
14393
|
+
|
|
14394
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
14395
|
+
|
|
14396
|
+
/** Load an image and pack it into a texture sheet
|
|
14397
|
+
* - Returns a TileInfo immediately which is filled in when the image loads
|
|
14398
|
+
* - Nothing is visible until it loads, use spritesReady to wait for it
|
|
14399
|
+
* - Pass frameSize for animations, then step through them with TileInfo.frame
|
|
14400
|
+
* - Grid images keep their layout and frames wrap down to the next row
|
|
14401
|
+
* @param {string} src - Image source path
|
|
14402
|
+
* @param {Vector2|number} [frameSize] - Size of each animation frame in pixels
|
|
14403
|
+
* @param {number} [padding] - How many pixels padding around each frame
|
|
14404
|
+
* @return {TileInfo}
|
|
14405
|
+
* @example
|
|
14406
|
+
* const playerTile = loadSprite('player.png'); // a single sprite
|
|
14407
|
+
* const runTile = loadSprite('run.png', vec2(16)); // a 16x16 frame animation
|
|
14408
|
+
* @memberof TextureSheets */
|
|
14409
|
+
function loadSprite(src, frameSize, padding=textureSheetPadding)
|
|
14410
|
+
{
|
|
14411
|
+
ASSERT(isStringLike(src), 'image src must be a string');
|
|
14412
|
+
ASSERT(!frameSize || isVector2(frameSize) || isNumber(frameSize), 'frameSize must be a vec2 or number');
|
|
14413
|
+
ASSERT(isNumber(padding), 'padding must be a number');
|
|
14414
|
+
|
|
14415
|
+
if (isNumber(frameSize))
|
|
14416
|
+
frameSize = vec2(frameSize);
|
|
14417
|
+
|
|
14418
|
+
// start with an empty tile that gets filled in when the image loads
|
|
14419
|
+
const tileInfo = new TileInfo(vec2(), vec2(), undefined, padding, 0);
|
|
14420
|
+
if (headlessMode) return tileInfo;
|
|
14421
|
+
|
|
14422
|
+
// point at a sheet right away so drawing before it loads picks up empty pixels
|
|
14423
|
+
tileInfo.textureInfo = (textureSheets[0] || textureSheetCreate()).textureInfo;
|
|
14424
|
+
|
|
14425
|
+
// start decoding right away, images decode in parallel
|
|
14426
|
+
const image = new Image;
|
|
14427
|
+
const imagePromise = new Promise(resolve =>
|
|
14428
|
+
{
|
|
14429
|
+
image.onerror = image.onload = resolve;
|
|
14430
|
+
image.crossOrigin = 'anonymous';
|
|
14431
|
+
image.src = src;
|
|
14432
|
+
});
|
|
14433
|
+
|
|
14434
|
+
// pack through a queue so sheets fill in call order, not decode order
|
|
14435
|
+
++textureSheetPendingCount;
|
|
14436
|
+
textureSheetQueue = textureSheetQueue.then(async ()=>
|
|
14437
|
+
{
|
|
14438
|
+
await imagePromise;
|
|
14439
|
+
if (image.width)
|
|
14440
|
+
{
|
|
14441
|
+
// pack onto a sheet, then fill in the tile that was already handed out,
|
|
14442
|
+
// copying every field so nothing is missed if TileInfo gains more of them
|
|
14443
|
+
const imageSize = vec2(image.width, image.height);
|
|
14444
|
+
const {sheet, tile} = textureSheetAdd(imageSize, frameSize, padding);
|
|
14445
|
+
Object.assign(tileInfo, tile);
|
|
14446
|
+
sheet.drawImage(image, tileInfo, false); // upload once per batch below
|
|
14447
|
+
}
|
|
14448
|
+
else
|
|
14449
|
+
{
|
|
14450
|
+
// leave the tile empty if the image failed to load
|
|
14451
|
+
LOG('loadSprite failed to load image:', src);
|
|
14452
|
+
}
|
|
14453
|
+
|
|
14454
|
+
// upload to webgl once per batch, when the last pending load finishes
|
|
14455
|
+
if (!--textureSheetPendingCount)
|
|
14456
|
+
textureSheets.forEach(s=> s.updateTexture());
|
|
14457
|
+
});
|
|
14458
|
+
|
|
14459
|
+
return tileInfo;
|
|
14460
|
+
}
|
|
14461
|
+
|
|
14462
|
+
/** Load a pre-packed texture atlas and repack it onto texture sheets
|
|
14463
|
+
* - Supports TexturePacker json (hash and array) and Aseprite json
|
|
14464
|
+
* - Returns an empty object which is filled with TileInfos when loaded
|
|
14465
|
+
* - Frames are named by the json, animations are grouped automatically
|
|
14466
|
+
* - Aseprite frame tags become animations, so do names like run_0, run_1
|
|
14467
|
+
* - Trimmed frames are restored to their full source size when packed
|
|
14468
|
+
* - Rotated frames are rotated back upright when packed
|
|
14469
|
+
* @param {string} imageSrc - Atlas image path
|
|
14470
|
+
* @param {string|Object} jsonSrc - Atlas json path, or already parsed json data
|
|
14471
|
+
* @param {number} [padding] - How many pixels padding around each frame
|
|
14472
|
+
* @return {Object} Object mapping frame and animation names to TileInfos
|
|
14473
|
+
* @example
|
|
14474
|
+
* const atlas = loadAtlas('sprites.png', 'sprites.json');
|
|
14475
|
+
* await spritesReady();
|
|
14476
|
+
* drawTile(pos, size, atlas.player); // a single frame
|
|
14477
|
+
* drawTile(pos, size, atlas.run.frame(2)); // frame 2 of the run animation
|
|
14478
|
+
* @memberof TextureSheets */
|
|
14479
|
+
function loadAtlas(imageSrc, jsonSrc, padding=textureSheetPadding)
|
|
14480
|
+
{
|
|
14481
|
+
ASSERT(isStringLike(imageSrc), 'atlas image src must be a string');
|
|
14482
|
+
ASSERT(isStringLike(jsonSrc) || typeof jsonSrc === 'object', 'atlas json must be a path or object');
|
|
14483
|
+
ASSERT(isNumber(padding), 'padding must be a number');
|
|
14484
|
+
|
|
14485
|
+
const atlas = {};
|
|
14486
|
+
if (headlessMode) return atlas;
|
|
14487
|
+
|
|
14488
|
+
// start fetching the json and decoding the image right away, in parallel
|
|
14489
|
+
const jsonPromise = typeof jsonSrc === 'object' ? Promise.resolve(jsonSrc) :
|
|
14490
|
+
fetch(jsonSrc).then(r=> r.ok && r.json()).catch(()=> undefined);
|
|
14491
|
+
const image = new Image;
|
|
14492
|
+
const imagePromise = new Promise(resolve =>
|
|
14493
|
+
{
|
|
14494
|
+
image.onerror = image.onload = resolve;
|
|
14495
|
+
image.crossOrigin = 'anonymous';
|
|
14496
|
+
image.src = imageSrc;
|
|
14497
|
+
});
|
|
14498
|
+
|
|
14499
|
+
// pack through a queue so sheets fill in call order, not decode order
|
|
14500
|
+
++textureSheetPendingCount;
|
|
14501
|
+
textureSheetQueue = textureSheetQueue.then(async ()=>
|
|
14502
|
+
{
|
|
14503
|
+
const data = await jsonPromise;
|
|
14504
|
+
await imagePromise;
|
|
14505
|
+
if (image.width && data)
|
|
14506
|
+
{
|
|
14507
|
+
for (const group of parseAtlas(data))
|
|
14508
|
+
{
|
|
14509
|
+
// reserve a block of full size cells, one per frame
|
|
14510
|
+
const sourceSize = group.frames[0].sourceSize;
|
|
14511
|
+
const blockSize = vec2(sourceSize.x*group.frames.length, sourceSize.y);
|
|
14512
|
+
const {sheet, tile} = textureSheetAdd(blockSize, sourceSize, padding);
|
|
14513
|
+
|
|
14514
|
+
// draw each frame untrimmed into its cell
|
|
14515
|
+
const context = sheet.context;
|
|
14516
|
+
const cellWidth = sourceSize.x + padding*2;
|
|
14517
|
+
const cellHeight = sourceSize.y + padding*2;
|
|
14518
|
+
group.frames.forEach((f, i)=>
|
|
14519
|
+
{
|
|
14520
|
+
const x = tile.pos.x + (i % tile.columns)*cellWidth + f.offset.x;
|
|
14521
|
+
const y = tile.pos.y + (i / tile.columns |0)*cellHeight + f.offset.y;
|
|
14522
|
+
if (f.rotated)
|
|
14523
|
+
{
|
|
14524
|
+
// stored rotated 90 degrees clockwise, draw it back upright
|
|
14525
|
+
context.save();
|
|
14526
|
+
context.translate(x, y);
|
|
14527
|
+
context.rotate(-PI/2);
|
|
14528
|
+
context.drawImage(image, f.pos.x, f.pos.y, f.size.y, f.size.x,
|
|
14529
|
+
-f.size.y, 0, f.size.y, f.size.x);
|
|
14530
|
+
context.restore();
|
|
14531
|
+
}
|
|
14532
|
+
else
|
|
14533
|
+
context.drawImage(image, f.pos.x, f.pos.y, f.size.x, f.size.y,
|
|
14534
|
+
x, y, f.size.x, f.size.y);
|
|
14535
|
+
});
|
|
14536
|
+
sheet.glDirty = true;
|
|
14537
|
+
atlas[group.name] = tile;
|
|
14538
|
+
}
|
|
14539
|
+
}
|
|
14540
|
+
else
|
|
14541
|
+
{
|
|
14542
|
+
// leave the atlas empty if either file failed to load
|
|
14543
|
+
LOG('loadAtlas failed to load:', imageSrc, jsonSrc);
|
|
14544
|
+
}
|
|
14545
|
+
|
|
14546
|
+
// upload to webgl once per batch, when the last pending load finishes
|
|
14547
|
+
if (!--textureSheetPendingCount)
|
|
14548
|
+
textureSheets.forEach(s=> s.updateTexture());
|
|
14549
|
+
});
|
|
14550
|
+
|
|
14551
|
+
return atlas;
|
|
14552
|
+
}
|
|
14553
|
+
|
|
14554
|
+
/** Parse atlas json into a list of named frame groups, used by loadAtlas
|
|
14555
|
+
* - Accepts TexturePacker json (hash and array) and Aseprite json
|
|
14556
|
+
* - Frames tagged in Aseprite or named like run_0, run_1 group into animations
|
|
14557
|
+
* @param {Object} data - Parsed atlas json data
|
|
14558
|
+
* @return {Array<Object>} List of {name, frames} groups in atlas order
|
|
14559
|
+
* @memberof TextureSheets */
|
|
14560
|
+
function parseAtlas(data)
|
|
14561
|
+
{
|
|
14562
|
+
ASSERT(!!data?.frames, 'unrecognized atlas format, expected TexturePacker or Aseprite json');
|
|
14563
|
+
|
|
14564
|
+
// normalize both hash and array frame layouts into a single list
|
|
14565
|
+
const frames = (isArray(data.frames) ?
|
|
14566
|
+
data.frames.map(f=> [f.filename, f]) : Object.entries(data.frames))
|
|
14567
|
+
.map(([name, f])=> ({
|
|
14568
|
+
name: name.replace(/\.[^.\\/]+$/, ''), // strip file extension
|
|
14569
|
+
pos: vec2(f.frame.x, f.frame.y),
|
|
14570
|
+
size: vec2(f.frame.w, f.frame.h),
|
|
14571
|
+
offset: vec2(f.spriteSourceSize?.x ?? 0, f.spriteSourceSize?.y ?? 0),
|
|
14572
|
+
sourceSize: vec2(f.sourceSize?.w ?? f.frame.w, f.sourceSize?.h ?? f.frame.h),
|
|
14573
|
+
rotated: !!f.rotated,
|
|
14574
|
+
}));
|
|
14575
|
+
|
|
14576
|
+
const groups = [];
|
|
14577
|
+
const tags = data.meta?.frameTags;
|
|
14578
|
+
if (tags?.length)
|
|
14579
|
+
{
|
|
14580
|
+
// aseprite tags are authoritative, untagged frames stay individual
|
|
14581
|
+
const tagged = new Set;
|
|
14582
|
+
for (const tag of tags)
|
|
14583
|
+
{
|
|
14584
|
+
groups.push({name: tag.name, frames: frames.slice(tag.from, tag.to + 1)});
|
|
14585
|
+
for (let i = tag.from; i <= tag.to; ++i)
|
|
14586
|
+
tagged.add(i);
|
|
14587
|
+
}
|
|
14588
|
+
frames.forEach((f, i)=> tagged.has(i) || groups.push({name: f.name, frames: [f]}));
|
|
14589
|
+
return groups;
|
|
14590
|
+
}
|
|
14591
|
+
|
|
14592
|
+
// group frames that share a name stem with contiguous trailing numbers
|
|
14593
|
+
// run_0.png and run_1.png become a 2 frame animation named run
|
|
14594
|
+
const stems = new Map;
|
|
14595
|
+
for (const f of frames)
|
|
14596
|
+
{
|
|
14597
|
+
let match = f.name.match(/^(.+?)([-_ ])?(\d+)$/);
|
|
14598
|
+
if (match && !match[2] && /\d$/.test(match[1]))
|
|
14599
|
+
match = undefined; // all digit tails like 10 are a name, not frame 0 of 1
|
|
14600
|
+
const stem = match ? match[1] : f.name;
|
|
14601
|
+
f.groupIndex = match ? Number(match[3]) : undefined;
|
|
14602
|
+
stems.has(stem) || stems.set(stem, []);
|
|
14603
|
+
stems.get(stem).push(f);
|
|
14604
|
+
}
|
|
14605
|
+
for (const [stem, list] of stems)
|
|
14606
|
+
{
|
|
14607
|
+
// only group 2 or more frames with contiguous indices and matching sizes
|
|
14608
|
+
list.sort((a, b)=> a.groupIndex - b.groupIndex);
|
|
14609
|
+
const grouped = list.length > 1 &&
|
|
14610
|
+
list.every((f, i)=> f.groupIndex === list[0].groupIndex + i) &&
|
|
14611
|
+
list.every(f=> f.sourceSize.x === list[0].sourceSize.x &&
|
|
14612
|
+
f.sourceSize.y === list[0].sourceSize.y);
|
|
14613
|
+
if (grouped)
|
|
14614
|
+
groups.push({name: stem, frames: list});
|
|
14615
|
+
else
|
|
14616
|
+
list.forEach(f=> groups.push({name: f.name, frames: [f]}));
|
|
14617
|
+
}
|
|
14618
|
+
return groups;
|
|
14619
|
+
}
|
|
14620
|
+
|
|
14621
|
+
/** Wait for everything started by loadSprite and loadAtlas to finish packing
|
|
14622
|
+
* @return {Promise}
|
|
14623
|
+
* @example
|
|
14624
|
+
* async function gameInit()
|
|
14625
|
+
* {
|
|
14626
|
+
* playerTile = loadSprite('player.png');
|
|
14627
|
+
* runTile = loadSprite('run.png', vec2(16));
|
|
14628
|
+
* await spritesReady();
|
|
14629
|
+
* }
|
|
14630
|
+
* @memberof TextureSheets */
|
|
14631
|
+
async function spritesReady()
|
|
14632
|
+
{
|
|
14633
|
+
// keep waiting until the queue drains, more sprites may load while waiting
|
|
14634
|
+
while (textureSheetPendingCount)
|
|
14635
|
+
await textureSheetQueue;
|
|
14636
|
+
}
|
|
14637
|
+
|
|
14638
|
+
// create a new texture sheet and add it to the list
|
|
14639
|
+
function textureSheetCreate()
|
|
14640
|
+
{
|
|
14641
|
+
const sheet = new TextureSheet;
|
|
14642
|
+
textureSheets.push(sheet);
|
|
14643
|
+
return sheet;
|
|
14644
|
+
}
|
|
14645
|
+
|
|
14646
|
+
// use the first sheet with enough space, or make a new one
|
|
14647
|
+
function textureSheetAdd(imageSize, frameSize, padding)
|
|
14648
|
+
{
|
|
14649
|
+
let sheet, tile;
|
|
14650
|
+
for (sheet of textureSheets)
|
|
14651
|
+
if (tile = sheet.tryAdd(imageSize, frameSize, padding))
|
|
14652
|
+
break;
|
|
14653
|
+
if (!tile)
|
|
14654
|
+
{
|
|
14655
|
+
sheet = textureSheetCreate();
|
|
14656
|
+
tile = sheet.tryAdd(imageSize, frameSize, padding);
|
|
14657
|
+
ASSERT(!!tile, 'image is too large to fit on a texture sheet');
|
|
14658
|
+
}
|
|
14659
|
+
return {sheet, tile};
|
|
14660
|
+
}
|
|
14661
|
+
|
|
14662
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
14663
|
+
// Texture sheet setting setters
|
|
14664
|
+
|
|
14665
|
+
/** Set width and height in pixels of texture sheets created by loadSprite
|
|
14666
|
+
* @param {number} size
|
|
14667
|
+
* @memberof Settings */
|
|
14668
|
+
function setTextureSheetSize(size) { textureSheetSize = size; }
|
|
14669
|
+
|
|
14670
|
+
/** Set default padding pixels around each frame packed by loadSprite
|
|
14671
|
+
* @param {number} padding
|
|
14672
|
+
* @memberof Settings */
|
|
14673
|
+
function setTextureSheetPadding(padding) { textureSheetPadding = padding; }
|
|
14674
|
+
|
|
14203
14675
|
/**
|
|
14204
14676
|
* LittleJS Tween System Plugin
|
|
14205
14677
|
* - Lightweight tweens for numbers, Vector2, Color, or any .lerp-able type
|
|
@@ -14626,7 +15098,11 @@ function tweenProperty(target, propertyPath, start, end, duration = 1, options =
|
|
|
14626
15098
|
const callback = (value) =>
|
|
14627
15099
|
{
|
|
14628
15100
|
let obj = target;
|
|
14629
|
-
for (const k of parts)
|
|
15101
|
+
for (const k of parts)
|
|
15102
|
+
{
|
|
15103
|
+
obj = obj[k];
|
|
15104
|
+
ASSERT(obj != null, 'tweenProperty path does not resolve: ' + propertyPath);
|
|
15105
|
+
}
|
|
14630
15106
|
obj[lastKey] = value;
|
|
14631
15107
|
};
|
|
14632
15108
|
return new Tween(callback, start, end, duration, options);
|
|
@@ -15509,3 +15985,157 @@ class PathFinder
|
|
|
15509
15985
|
}
|
|
15510
15986
|
}
|
|
15511
15987
|
|
|
15988
|
+
/**
|
|
15989
|
+
* LittleJS Three.js Plugin
|
|
15990
|
+
* - Renders a three.js scene on a canvas behind the LittleJS canvases
|
|
15991
|
+
* - The three.js module is passed in by the user, nothing is bundled
|
|
15992
|
+
* - Keep canvasClearColor transparent so the 3D scene shows through
|
|
15993
|
+
* - Aligned camera mode locks the 3D camera to the LittleJS 2D camera
|
|
15994
|
+
* - ThreeJSObject lets LittleJS physics drive a three.js mesh
|
|
15995
|
+
* - Call new ThreeJSPlugin(THREE) in gameInit to set up
|
|
15996
|
+
* @namespace ThreeJS
|
|
15997
|
+
*/
|
|
15998
|
+
|
|
15999
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
16000
|
+
|
|
16001
|
+
/** Global ThreeJS plugin object
|
|
16002
|
+
* @type {ThreeJSPlugin}
|
|
16003
|
+
* @memberof ThreeJS */
|
|
16004
|
+
let threeJS;
|
|
16005
|
+
|
|
16006
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
16007
|
+
/**
|
|
16008
|
+
* ThreeJS Plugin - Renders a three.js scene behind the LittleJS canvas
|
|
16009
|
+
* @example
|
|
16010
|
+
* // in gameInit, with three.js loaded by the user
|
|
16011
|
+
* new ThreeJSPlugin(THREE);
|
|
16012
|
+
* threeJS.scene.add(new THREE.AmbientLight);
|
|
16013
|
+
* @memberof ThreeJS
|
|
16014
|
+
*/
|
|
16015
|
+
class ThreeJSPlugin
|
|
16016
|
+
{
|
|
16017
|
+
/** Set up the three.js rendering layer, call in gameInit
|
|
16018
|
+
* @param {Object} THREE - The three.js module, supplied by the user
|
|
16019
|
+
* @param {number} [cameraFOV] - Vertical field of view in degrees */
|
|
16020
|
+
constructor(THREE, cameraFOV=60)
|
|
16021
|
+
{
|
|
16022
|
+
ASSERT(!threeJS, 'ThreeJS plugin already initialized');
|
|
16023
|
+
threeJS = this;
|
|
16024
|
+
if (headlessMode) return;
|
|
16025
|
+
ASSERT(mainCanvas, 'ThreeJS plugin must be created after engineInit, call in gameInit');
|
|
16026
|
+
ASSERT(THREE && THREE.WebGLRenderer, 'three.js module must be passed in');
|
|
16027
|
+
|
|
16028
|
+
/** @property {Object} - The three.js module passed into the constructor */
|
|
16029
|
+
this.THREE = THREE;
|
|
16030
|
+
/** @property {Object} - The three.js renderer */
|
|
16031
|
+
this.renderer = new THREE.WebGLRenderer({antialias: true});
|
|
16032
|
+
/** @property {Object} - The three.js scene, add lights and meshes here */
|
|
16033
|
+
this.scene = new THREE.Scene();
|
|
16034
|
+
/** @property {Object} - The three.js perspective camera */
|
|
16035
|
+
this.camera = new THREE.PerspectiveCamera(cameraFOV, 1, .1, 1e3);
|
|
16036
|
+
/** @property {boolean} - Lock the camera to the LittleJS 2D camera so the z=0 plane matches world space */
|
|
16037
|
+
this.cameraAlign2D = true;
|
|
16038
|
+
|
|
16039
|
+
// insert the canvas below the engine canvases and match the layout
|
|
16040
|
+
const threeCanvas = this.renderer.domElement;
|
|
16041
|
+
const rootElement = mainCanvas.parentElement;
|
|
16042
|
+
rootElement.insertBefore(threeCanvas, rootElement.firstChild);
|
|
16043
|
+
threeCanvas.style.cssText = mainCanvas.style.cssText;
|
|
16044
|
+
|
|
16045
|
+
// render automatically each frame after the engine renders
|
|
16046
|
+
engineAddPlugin(undefined, ()=> this.render());
|
|
16047
|
+
}
|
|
16048
|
+
|
|
16049
|
+
/** Position the camera so the z=0 plane exactly matches LittleJS world space,
|
|
16050
|
+
* called automatically when cameraAlign2D is set */
|
|
16051
|
+
alignCamera2D()
|
|
16052
|
+
{
|
|
16053
|
+
const halfHeight = mainCanvasSize.y / 2 / cameraScale; // half visible height in world units
|
|
16054
|
+
const distance = halfHeight / tan(this.camera.fov/2 * PI/180);
|
|
16055
|
+
this.camera.position.set(cameraPos.x, cameraPos.y, distance);
|
|
16056
|
+
// reset all axes in case a free camera was used, littlejs angles are clockwise
|
|
16057
|
+
this.camera.rotation.set(0, 0, -cameraAngle);
|
|
16058
|
+
}
|
|
16059
|
+
|
|
16060
|
+
/** Sync the canvas layout and render the scene, called automatically each frame */
|
|
16061
|
+
render()
|
|
16062
|
+
{
|
|
16063
|
+
if (!this.renderer) return; // headless mode
|
|
16064
|
+
|
|
16065
|
+
// keep renderer size and css in sync with the LittleJS canvas
|
|
16066
|
+
const threeCanvas = this.renderer.domElement;
|
|
16067
|
+
if (threeCanvas.width != mainCanvasSize.x || threeCanvas.height != mainCanvasSize.y)
|
|
16068
|
+
{
|
|
16069
|
+
this.renderer.setSize(mainCanvasSize.x, mainCanvasSize.y, false);
|
|
16070
|
+
this.camera.aspect = mainCanvasSize.x / mainCanvasSize.y;
|
|
16071
|
+
this.camera.updateProjectionMatrix();
|
|
16072
|
+
}
|
|
16073
|
+
if (threeCanvas.style.cssText != mainCanvas.style.cssText)
|
|
16074
|
+
threeCanvas.style.cssText = mainCanvas.style.cssText;
|
|
16075
|
+
|
|
16076
|
+
if (this.cameraAlign2D)
|
|
16077
|
+
this.alignCamera2D();
|
|
16078
|
+
this.renderer.render(this.scene, this.camera);
|
|
16079
|
+
}
|
|
16080
|
+
}
|
|
16081
|
+
|
|
16082
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
16083
|
+
/**
|
|
16084
|
+
* ThreeJS Object - EngineObject that drives a three.js mesh
|
|
16085
|
+
* - LittleJS physics moves the object and the mesh follows automatically
|
|
16086
|
+
* - Destroying the object removes the mesh from the scene
|
|
16087
|
+
* @extends EngineObject
|
|
16088
|
+
* @memberof ThreeJS
|
|
16089
|
+
*/
|
|
16090
|
+
class ThreeJSObject extends EngineObject
|
|
16091
|
+
{
|
|
16092
|
+
/** Create an engine object that drives a three.js mesh
|
|
16093
|
+
* @param {Vector2} [pos] - World space position
|
|
16094
|
+
* @param {Vector2} [size] - World space size
|
|
16095
|
+
* @param {Object} [mesh] - The three.js object3d to drive
|
|
16096
|
+
* @param {number} [z] - Mesh height above the 2D plane */
|
|
16097
|
+
constructor(pos, size, mesh, z=0)
|
|
16098
|
+
{
|
|
16099
|
+
super(pos, size);
|
|
16100
|
+
ASSERT(threeJS, 'ThreeJS plugin must be initialized first');
|
|
16101
|
+
|
|
16102
|
+
/** @property {Object} - The three.js object3d this object drives */
|
|
16103
|
+
this.mesh = mesh;
|
|
16104
|
+
/** @property {number} - Mesh height above the 2D plane */
|
|
16105
|
+
this.z = z;
|
|
16106
|
+
if (mesh)
|
|
16107
|
+
{
|
|
16108
|
+
threeJS.scene.add(mesh);
|
|
16109
|
+
this.syncMesh();
|
|
16110
|
+
}
|
|
16111
|
+
}
|
|
16112
|
+
|
|
16113
|
+
/** Update the object and sync the mesh to its transform */
|
|
16114
|
+
update()
|
|
16115
|
+
{
|
|
16116
|
+
super.update();
|
|
16117
|
+
this.syncMesh();
|
|
16118
|
+
}
|
|
16119
|
+
|
|
16120
|
+
/** Copy this object's transform to the mesh */
|
|
16121
|
+
syncMesh()
|
|
16122
|
+
{
|
|
16123
|
+
if (!this.mesh) return;
|
|
16124
|
+
this.mesh.position.set(this.pos.x, this.pos.y, this.z);
|
|
16125
|
+
this.mesh.rotation.z = -this.angle; // littlejs angles are clockwise
|
|
16126
|
+
}
|
|
16127
|
+
|
|
16128
|
+
/** The mesh is this object's visual, the default 2D rendering is skipped */
|
|
16129
|
+
render() {}
|
|
16130
|
+
|
|
16131
|
+
/** Destroy this object and remove its mesh from the scene
|
|
16132
|
+
* @param {boolean} [immediate] */
|
|
16133
|
+
destroy(immediate)
|
|
16134
|
+
{
|
|
16135
|
+
if (this.destroyed) return;
|
|
16136
|
+
// note: frequently destroyed objects should also dispose geometry and material
|
|
16137
|
+
this.mesh && threeJS.scene.remove(this.mesh);
|
|
16138
|
+
super.destroy(immediate);
|
|
16139
|
+
}
|
|
16140
|
+
}
|
|
16141
|
+
|