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.esm.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();
|
|
@@ -1237,6 +1241,7 @@ function debugVideoCaptureStart()
|
|
|
1237
1241
|
{
|
|
1238
1242
|
LOG('Video capture not supported in this browser!');
|
|
1239
1243
|
silentAudioSource?.stop();
|
|
1244
|
+
audioStreamDestination && audioMasterGain.disconnect(audioStreamDestination);
|
|
1240
1245
|
return;
|
|
1241
1246
|
}
|
|
1242
1247
|
|
|
@@ -1265,6 +1270,8 @@ function debugVideoCaptureStop()
|
|
|
1265
1270
|
debugVideoCapture.silentAudioSource?.stop();
|
|
1266
1271
|
debugVideoCapture.mediaRecorder?.stop();
|
|
1267
1272
|
debugVideoCapture.videoTrack?.stop();
|
|
1273
|
+
if (debugVideoCapture.audioStreamDestination)
|
|
1274
|
+
audioMasterGain.disconnect(debugVideoCapture.audioStreamDestination);
|
|
1268
1275
|
debugVideoCapture = undefined;
|
|
1269
1276
|
}
|
|
1270
1277
|
|
|
@@ -1503,7 +1510,7 @@ function smoothStep(percent) { return percent * percent * (3 - 2 * percent); }
|
|
|
1503
1510
|
* @param {number} value
|
|
1504
1511
|
* @return {boolean}
|
|
1505
1512
|
* @memberof Math */
|
|
1506
|
-
function isPowerOfTwo(value) { return !(value & (value - 1)); }
|
|
1513
|
+
function isPowerOfTwo(value) { return value > 0 && !(value & (value - 1)); }
|
|
1507
1514
|
|
|
1508
1515
|
/** Returns the nearest power of two not less than the value
|
|
1509
1516
|
* @param {number} value
|
|
@@ -1580,7 +1587,7 @@ function isIntersecting(start, end, pos, size)
|
|
|
1580
1587
|
* @memberof Math */
|
|
1581
1588
|
function oscillate(frequency=1, amplitude=1, t=time, offset=0, type=0)
|
|
1582
1589
|
{
|
|
1583
|
-
const phase = (offset + t*frequency
|
|
1590
|
+
const phase = mod(offset + t*frequency, 1);
|
|
1584
1591
|
let value;
|
|
1585
1592
|
|
|
1586
1593
|
if (type === 1) // triangle
|
|
@@ -2516,7 +2523,7 @@ const MAGENTA = debugProtectConstant(rgb(1,0,1));
|
|
|
2516
2523
|
class Timer
|
|
2517
2524
|
{
|
|
2518
2525
|
/** Create a timer object set time passed in
|
|
2519
|
-
* @param {number} [timeLeft] - How much time left before the timer
|
|
2526
|
+
* @param {number} [timeLeft] - How much time left before the timer is elapsed in seconds (undefined = unset)
|
|
2520
2527
|
* @param {boolean} [useRealTime] - Should the timer keep running even when the game is paused? (useful for UI) */
|
|
2521
2528
|
constructor(timeLeft, useRealTime=false)
|
|
2522
2529
|
{
|
|
@@ -3115,6 +3122,7 @@ let vibrateEnable = true;
|
|
|
3115
3122
|
let soundEnable = true;
|
|
3116
3123
|
|
|
3117
3124
|
/** Volume scale to apply to all sound, music and speech
|
|
3125
|
+
* Use setSoundVolume to also update the audio master gain immediately
|
|
3118
3126
|
* @type {number}
|
|
3119
3127
|
* @default
|
|
3120
3128
|
* @memberof Settings */
|
|
@@ -4159,8 +4167,9 @@ class TileInfo
|
|
|
4159
4167
|
* @param {TextureInfo} [textureInfo] - Texture info to use
|
|
4160
4168
|
* @param {number} [padding] - How many pixels padding around all sides of each tile (increases grid size, does not affect tile size)
|
|
4161
4169
|
* @param {number} [bleed] - How many pixels smaller to shrink UVS of tiles (does not affect grid size, only UVs)
|
|
4170
|
+
* @param {number} [columns] - How many frames per row for frame(), 0 to keep frames on a single row
|
|
4162
4171
|
*/
|
|
4163
|
-
constructor(pos=vec2(), size=tileDefaultSize, textureInfo=textureInfos[0], padding=tileDefaultPadding, bleed=tileDefaultBleed)
|
|
4172
|
+
constructor(pos=vec2(), size=tileDefaultSize, textureInfo=textureInfos[0], padding=tileDefaultPadding, bleed=tileDefaultBleed, columns=0)
|
|
4164
4173
|
{
|
|
4165
4174
|
/** @property {Vector2} - Top left corner of tile in pixels */
|
|
4166
4175
|
this.pos = pos.copy();
|
|
@@ -4172,6 +4181,8 @@ class TileInfo
|
|
|
4172
4181
|
this.textureInfo = textureInfo;
|
|
4173
4182
|
/** @property {number} - Shrinks tile by this many pixels to prevent neighbors bleeding */
|
|
4174
4183
|
this.bleed = bleed;
|
|
4184
|
+
/** @property {number} - How many frames per row for frame(), 0 to keep frames on a single row */
|
|
4185
|
+
this.columns = columns;
|
|
4175
4186
|
}
|
|
4176
4187
|
|
|
4177
4188
|
/** Returns a copy of this tile offset by a vector
|
|
@@ -4179,9 +4190,10 @@ class TileInfo
|
|
|
4179
4190
|
* @return {TileInfo}
|
|
4180
4191
|
*/
|
|
4181
4192
|
offset(offset)
|
|
4182
|
-
{ return new TileInfo(this.pos.add(offset), this.size, this.textureInfo, this.padding, this.bleed); }
|
|
4193
|
+
{ return new TileInfo(this.pos.add(offset), this.size, this.textureInfo, this.padding, this.bleed, this.columns); }
|
|
4183
4194
|
|
|
4184
4195
|
/** Returns a copy of this tile offset by a number of animation frames
|
|
4196
|
+
* Frames wrap down to the next row if columns is set
|
|
4185
4197
|
* @param {number} frame - Offset to apply in animation frames
|
|
4186
4198
|
* @return {TileInfo}
|
|
4187
4199
|
*/
|
|
@@ -4189,11 +4201,33 @@ class TileInfo
|
|
|
4189
4201
|
{
|
|
4190
4202
|
ASSERT(typeof frame === 'number');
|
|
4191
4203
|
const w = this.size.x + this.padding*2;
|
|
4192
|
-
const
|
|
4193
|
-
|
|
4194
|
-
|
|
4204
|
+
const h = this.size.y + this.padding*2;
|
|
4205
|
+
const x = (this.columns ? frame % this.columns : frame) * w;
|
|
4206
|
+
const y = (this.columns ? frame / this.columns | 0 : 0) * h;
|
|
4207
|
+
ASSERT(this.pos.x + x + this.size.x <= this.textureInfo.size.x, 'frame extends beyond texture width!');
|
|
4208
|
+
ASSERT(this.pos.y + y + this.size.y <= this.textureInfo.size.y, 'frame extends beyond texture height!');
|
|
4209
|
+
return this.offset(new Vector2(x, y));
|
|
4210
|
+
}
|
|
4211
|
+
|
|
4212
|
+
/** Set how many frames per row this tile uses, so frame() can wrap
|
|
4213
|
+
* @param {number} [columns] - Frames per row, 0 to keep frames on a single row
|
|
4214
|
+
* @return {TileInfo}
|
|
4215
|
+
*/
|
|
4216
|
+
setColumns(columns=0)
|
|
4217
|
+
{
|
|
4218
|
+
ASSERT(isNumber(columns) && columns >= 0, 'columns must be a number >= 0');
|
|
4219
|
+
this.columns = columns;
|
|
4220
|
+
return this;
|
|
4195
4221
|
}
|
|
4196
4222
|
|
|
4223
|
+
/**
|
|
4224
|
+
* Returns a tile info for an index using this tile as reference
|
|
4225
|
+
* @param {Vector2|number} [index=0]
|
|
4226
|
+
* @return {TileInfo}
|
|
4227
|
+
*/
|
|
4228
|
+
index(index)
|
|
4229
|
+
{ return tile(index, this.size, this.textureInfo, this.padding, this.bleed).setColumns(this.columns); }
|
|
4230
|
+
|
|
4197
4231
|
/**
|
|
4198
4232
|
* Set this tile to use a full image in a texture info
|
|
4199
4233
|
* @param {TextureInfo} [textureInfo]
|
|
@@ -4204,17 +4238,9 @@ class TileInfo
|
|
|
4204
4238
|
this.textureInfo = textureInfo;
|
|
4205
4239
|
this.pos = new Vector2;
|
|
4206
4240
|
this.size = textureInfo.size.copy();
|
|
4207
|
-
this.bleed = this.padding = 0;
|
|
4241
|
+
this.bleed = this.padding = this.columns = 0;
|
|
4208
4242
|
return this;
|
|
4209
4243
|
}
|
|
4210
|
-
|
|
4211
|
-
/**
|
|
4212
|
-
* Returns a tile info for an index using this tile as reference
|
|
4213
|
-
* @param {Vector2|number} [index=0]
|
|
4214
|
-
* @return {TileInfo}
|
|
4215
|
-
*/
|
|
4216
|
-
tile(index)
|
|
4217
|
-
{ return tile(index, this.size, this.textureInfo, this.padding, this.bleed); }
|
|
4218
4244
|
}
|
|
4219
4245
|
|
|
4220
4246
|
/**
|
|
@@ -5912,7 +5938,10 @@ function inputInit()
|
|
|
5912
5938
|
{
|
|
5913
5939
|
inputData[0][e.code] = (inputData[0][e.code]&2) | 4;
|
|
5914
5940
|
if (inputWASDEmulateDirection)
|
|
5915
|
-
|
|
5941
|
+
{
|
|
5942
|
+
const remap = remapKey(e.code);
|
|
5943
|
+
inputData[0][remap] = (inputData[0][remap]&2) | 4;
|
|
5944
|
+
}
|
|
5916
5945
|
}
|
|
5917
5946
|
function remapKey(k)
|
|
5918
5947
|
{
|
|
@@ -5987,7 +6016,7 @@ function inputInit()
|
|
|
5987
6016
|
document.addEventListener('touchend', (e)=> handleTouch(e), { passive: false });
|
|
5988
6017
|
|
|
5989
6018
|
// handle all touch events the same way
|
|
5990
|
-
let wasTouching;
|
|
6019
|
+
let wasTouching, touchIdentifier;
|
|
5991
6020
|
function handleTouch(e)
|
|
5992
6021
|
{
|
|
5993
6022
|
if (!touchInputEnable) return;
|
|
@@ -6018,10 +6047,11 @@ function inputInit()
|
|
|
6018
6047
|
const pos = vec2(gameTouches[0].clientX, gameTouches[0].clientY);
|
|
6019
6048
|
const mousePosScreenLast = mousePosScreen;
|
|
6020
6049
|
mousePosScreen = mouseEventToScreen(pos);
|
|
6021
|
-
if (wasTouching)
|
|
6050
|
+
if (wasTouching && gameTouches[0].identifier === touchIdentifier)
|
|
6022
6051
|
mouseDeltaScreen = mouseDeltaScreen.add(mousePosScreen.subtract(mousePosScreenLast));
|
|
6023
|
-
else
|
|
6052
|
+
else if (!wasTouching)
|
|
6024
6053
|
inputData[0][button] = 3;
|
|
6054
|
+
touchIdentifier = gameTouches[0].identifier;
|
|
6025
6055
|
}
|
|
6026
6056
|
else if (wasTouching)
|
|
6027
6057
|
inputData[0][button] = inputData[0][button] & 2 | 4;
|
|
@@ -7179,7 +7209,7 @@ class SoundInstance
|
|
|
7179
7209
|
* @param {number} [rate] - How quickly to speak
|
|
7180
7210
|
* @param {number} [pitch] - How much to change the pitch by
|
|
7181
7211
|
* @param {string} [language] - The language/accent to use (examples: en, it, ru, ja, zh)
|
|
7182
|
-
* @return {SpeechSynthesisUtterance} - The utterance that was spoken
|
|
7212
|
+
* @return {SpeechSynthesisUtterance|undefined} - The utterance that was spoken, or undefined if speech is unavailable
|
|
7183
7213
|
* @memberof Audio */
|
|
7184
7214
|
function speak(text, volume=1, rate=1, pitch=1, language='')
|
|
7185
7215
|
{
|
|
@@ -7232,7 +7262,7 @@ function getNoteFrequency(semitoneOffset, rootFrequency=220)
|
|
|
7232
7262
|
* @param {number} [pan] - How much to apply stereo panning
|
|
7233
7263
|
* @param {boolean} [loop] - True if the sound should loop when it reaches the end
|
|
7234
7264
|
* @param {number} [sampleRate=44100] - Sample rate for the sound
|
|
7235
|
-
* @param {GainNode} [gainNode] - Optional gain node for volume control while playing
|
|
7265
|
+
* @param {GainNode} [gainNode] - Optional gain node for volume control while playing (disconnected when the sound ends)
|
|
7236
7266
|
* @param {number} [offset] - Offset in seconds to start playback from
|
|
7237
7267
|
* @param {AudioEndedCallback} [onended] - Callback for when the sound ends
|
|
7238
7268
|
* @return {AudioBufferSourceNode} - The source node of the sound played, may be undefined if play fails
|
|
@@ -7474,10 +7504,14 @@ function tileCollisionGetData(pos, solidOnly=true)
|
|
|
7474
7504
|
// check all tile collision layers
|
|
7475
7505
|
for (const layer of tileCollisionLayers)
|
|
7476
7506
|
if (!solidOnly || layer.isSolid)
|
|
7477
|
-
if (pos.arrayCheck(layer.size))
|
|
7478
7507
|
{
|
|
7479
|
-
|
|
7480
|
-
|
|
7508
|
+
// convert world pos to layer local space
|
|
7509
|
+
const layerPos = pos.subtract(layer.pos);
|
|
7510
|
+
if (layerPos.arrayCheck(layer.size))
|
|
7511
|
+
{
|
|
7512
|
+
const data = layer.getCollisionData(layerPos);
|
|
7513
|
+
if (data) return data;
|
|
7514
|
+
}
|
|
7481
7515
|
}
|
|
7482
7516
|
return 0;
|
|
7483
7517
|
}
|
|
@@ -7752,7 +7786,7 @@ class TileLayer extends CanvasLayer
|
|
|
7752
7786
|
|
|
7753
7787
|
/** @property {TileInfo} - Default tile info for layer */
|
|
7754
7788
|
this.tileInfo = undefined;
|
|
7755
|
-
/** @property {Array<TileLayerData>} -
|
|
7789
|
+
/** @property {Array<TileLayerData>} - Array of tile data for the layer */
|
|
7756
7790
|
this.data = [];
|
|
7757
7791
|
/** @property {boolean} - Is this layer using a webgl texture? */
|
|
7758
7792
|
this.isUsingWebGL = false;
|
|
@@ -7837,7 +7871,7 @@ class TileLayer extends CanvasLayer
|
|
|
7837
7871
|
|
|
7838
7872
|
const size = this.drawSize || this.size;
|
|
7839
7873
|
const pos = this.pos.add(size.scale(.5));
|
|
7840
|
-
this.draw(pos,
|
|
7874
|
+
this.draw(pos, size, this.color, this.angle, this.mirror, this.additiveColor);
|
|
7841
7875
|
}
|
|
7842
7876
|
|
|
7843
7877
|
/** Called after this layer is redrawn, does nothing by default */
|
|
@@ -7926,7 +7960,7 @@ class TileLayer extends CanvasLayer
|
|
|
7926
7960
|
const d = this.getData(layerPos);
|
|
7927
7961
|
if (!d || !d.tile) return;
|
|
7928
7962
|
|
|
7929
|
-
const tileInfo = this.tileInfo && this.tileInfo.
|
|
7963
|
+
const tileInfo = this.tileInfo && this.tileInfo.index(d.tile);
|
|
7930
7964
|
this.drawLayerTile(drawPos, drawSize, tileInfo, d.color, d.direction*PI/2, d.mirror);
|
|
7931
7965
|
}
|
|
7932
7966
|
|
|
@@ -8220,8 +8254,8 @@ class TileCollisionLayer extends TileLayer
|
|
|
8220
8254
|
* rgb(1,1,1,1), rgb(0,0,0,1), // colorStartA, colorStartB
|
|
8221
8255
|
* rgb(1,1,1,0), rgb(0,0,0,0), // colorEndA, colorEndB
|
|
8222
8256
|
* 1, .2, .2, .1, .05, // particleTime, sizeStart, sizeEnd, particleSpeed, particleAngleSpeed
|
|
8223
|
-
* .99, 1, 1, PI, .05, // damping, angleDamping, gravityScale, particleCone, fadeRate
|
|
8224
|
-
* .5, 1 // randomness, collide
|
|
8257
|
+
* .99, 1, 1, PI, .05, // damping, angleDamping, gravityScale, particleCone, fadeRate
|
|
8258
|
+
* .5, 1 // randomness, collide
|
|
8225
8259
|
* );
|
|
8226
8260
|
*/
|
|
8227
8261
|
class ParticleEmitter extends EngineObject
|
|
@@ -8613,13 +8647,12 @@ class Particle
|
|
|
8613
8647
|
const hitLayer = tileCollisionTest(this.pos);
|
|
8614
8648
|
if (!testCollision(oldPos))
|
|
8615
8649
|
{
|
|
8616
|
-
// testCollision already invoked collideCallback with the
|
|
8617
|
-
// correct (this, data, pos) args; no need to re-check here.
|
|
8618
8650
|
// test which side we bounced off (or both if a corner)
|
|
8619
8651
|
const isBlockedX = testCollision(vec2(this.pos.x, oldPos.y));
|
|
8620
8652
|
const isBlockedY = testCollision(vec2(oldPos.x, this.pos.y));
|
|
8621
|
-
|
|
8622
|
-
const
|
|
8653
|
+
// collide callback may hit where the layer test does not, so hitLayer can be undefined
|
|
8654
|
+
const hitRestitution = hitLayer ? max(restitution, hitLayer.restitution) : restitution;
|
|
8655
|
+
const hitFriction = hitLayer ? max(friction, hitLayer.friction) : friction;
|
|
8623
8656
|
if (isBlockedX)
|
|
8624
8657
|
{
|
|
8625
8658
|
// move to previous X position and bounce
|
|
@@ -10268,7 +10301,8 @@ class NewgroundsPlugin
|
|
|
10268
10301
|
return;
|
|
10269
10302
|
}
|
|
10270
10303
|
debugMedals && LOG(xmlHttp.responseText);
|
|
10271
|
-
return xmlHttp.responseText && JSON.parse(xmlHttp.responseText);
|
|
10304
|
+
try { return xmlHttp.responseText && JSON.parse(xmlHttp.responseText); }
|
|
10305
|
+
catch(e) { debugMedals && LOG('newgrounds response is not valid JSON', e); }
|
|
10272
10306
|
}
|
|
10273
10307
|
}
|
|
10274
10308
|
/**
|
|
@@ -10287,8 +10321,8 @@ class NewgroundsPlugin
|
|
|
10287
10321
|
let postProcess;
|
|
10288
10322
|
|
|
10289
10323
|
/////////////////////////////////////////////////////////////////////////
|
|
10290
|
-
/**
|
|
10291
|
-
*
|
|
10324
|
+
/**
|
|
10325
|
+
* Post Process Plugin - Applies a full screen shader to the rendered output
|
|
10292
10326
|
* @memberof PostProcess
|
|
10293
10327
|
*/
|
|
10294
10328
|
class PostProcessPlugin
|
|
@@ -10402,8 +10436,9 @@ class PostProcessPlugin
|
|
|
10402
10436
|
workCanvas.height = mainCanvasSize.y;
|
|
10403
10437
|
glCopyToContext(workContext);
|
|
10404
10438
|
workContext.drawImage(mainCanvas, 0, 0);
|
|
10405
|
-
mainCanvas.width |= 0
|
|
10406
|
-
|
|
10439
|
+
mainCanvas.width |= 0; // setting size clears the main canvas
|
|
10440
|
+
|
|
10441
|
+
|
|
10407
10442
|
// copy work canvas to texture
|
|
10408
10443
|
glContext.texImage2D(glContext.TEXTURE_2D, 0, glContext.RGBA, glContext.RGBA, glContext.UNSIGNED_BYTE, workCanvas);
|
|
10409
10444
|
}
|
|
@@ -11508,7 +11543,7 @@ class UISystemPlugin
|
|
|
11508
11543
|
}
|
|
11509
11544
|
|
|
11510
11545
|
/** Get other axis navigation direction from gamepad or keyboard
|
|
11511
|
-
* @return {
|
|
11546
|
+
* @return {number} */
|
|
11512
11547
|
getNavigationOtherDirection()
|
|
11513
11548
|
{
|
|
11514
11549
|
if (uiSystem.navigationDirection === 2)
|
|
@@ -11597,6 +11632,7 @@ class UISystemPlugin
|
|
|
11597
11632
|
uiSystem.navigationDirection = savedNavigationDirection;
|
|
11598
11633
|
inputClear();
|
|
11599
11634
|
}
|
|
11635
|
+
return confirmMenu;
|
|
11600
11636
|
}
|
|
11601
11637
|
}
|
|
11602
11638
|
|
|
@@ -12030,7 +12066,7 @@ class UITextInput extends UIObject
|
|
|
12030
12066
|
this.onClick();
|
|
12031
12067
|
}
|
|
12032
12068
|
|
|
12033
|
-
/** Stop editing the text
|
|
12069
|
+
/** Stop editing the text */
|
|
12034
12070
|
stopEditing()
|
|
12035
12071
|
{
|
|
12036
12072
|
if (!this.isKeyInputObject())
|
|
@@ -12193,7 +12229,7 @@ class UICheckbox extends UIObject
|
|
|
12193
12229
|
ASSERT(isStringLike(text), 'ui checkbox must be a string');
|
|
12194
12230
|
ASSERT(isColor(color), 'ui checkbox color must be a color');
|
|
12195
12231
|
|
|
12196
|
-
/** @property {boolean} -
|
|
12232
|
+
/** @property {boolean} - Is the checkbox currently checked? */
|
|
12197
12233
|
this.checked = checked;
|
|
12198
12234
|
// set properties
|
|
12199
12235
|
this.text = text;
|
|
@@ -12958,7 +12994,7 @@ class Box2dObject extends EngineObject
|
|
|
12958
12994
|
shape.set_m_vertex3(box2d.vec2dTo(getPoint(i+2)));
|
|
12959
12995
|
const f = this.addShape(shape, density, friction, restitution, isSensor);
|
|
12960
12996
|
fixtures.push(f);
|
|
12961
|
-
|
|
12997
|
+
edgePoints.push(points[i].copy());
|
|
12962
12998
|
}
|
|
12963
12999
|
this.edgeLoops.push(edgePoints);
|
|
12964
13000
|
return fixtures;
|
|
@@ -13024,7 +13060,7 @@ class Box2dObject extends EngineObject
|
|
|
13024
13060
|
/** Sets the position
|
|
13025
13061
|
* @param {Vector2} pos */
|
|
13026
13062
|
setPosition(pos)
|
|
13027
|
-
{ this.setTransform(pos, this.body.GetAngle()); }
|
|
13063
|
+
{ this.setTransform(pos, -this.body.GetAngle()); }
|
|
13028
13064
|
|
|
13029
13065
|
/** Sets the angle
|
|
13030
13066
|
* @param {number} angle */
|
|
@@ -13121,6 +13157,7 @@ class Box2dObject extends EngineObject
|
|
|
13121
13157
|
filter.set_categoryBits(categoryBits);
|
|
13122
13158
|
filter.set_maskBits(0xffff & ~ignoreCategoryBits);
|
|
13123
13159
|
filter.set_groupIndex(groupIndex);
|
|
13160
|
+
fixture.SetFilterData(filter); // applies and refilters contacts
|
|
13124
13161
|
});
|
|
13125
13162
|
}
|
|
13126
13163
|
|
|
@@ -13656,7 +13693,7 @@ class Box2dRevoluteJoint extends Box2dJoint
|
|
|
13656
13693
|
jointDef.set_bodyB(objectB.body);
|
|
13657
13694
|
jointDef.set_localAnchorA(box2d.vec2dTo(localAnchorA));
|
|
13658
13695
|
jointDef.set_localAnchorB(box2d.vec2dTo(localAnchorB));
|
|
13659
|
-
jointDef.set_referenceAngle(
|
|
13696
|
+
jointDef.set_referenceAngle(objectB.body.GetAngle() - objectA.body.GetAngle());
|
|
13660
13697
|
jointDef.set_collideConnected(collide);
|
|
13661
13698
|
super(jointDef);
|
|
13662
13699
|
}
|
|
@@ -13803,14 +13840,14 @@ class Box2dPrismaticJoint extends Box2dJoint
|
|
|
13803
13840
|
anchor ||= box2d.vec2From(objectB.body.GetPosition());
|
|
13804
13841
|
const localAnchorA = objectA.worldToLocal(anchor);
|
|
13805
13842
|
const localAnchorB = objectB.worldToLocal(anchor);
|
|
13806
|
-
const localAxisA =
|
|
13843
|
+
const localAxisA = objectA.worldToLocalVector(worldAxis);
|
|
13807
13844
|
const jointDef = new box2d.instance.b2PrismaticJointDef();
|
|
13808
13845
|
jointDef.set_bodyA(objectA.body);
|
|
13809
13846
|
jointDef.set_bodyB(objectB.body);
|
|
13810
13847
|
jointDef.set_localAnchorA(box2d.vec2dTo(localAnchorA));
|
|
13811
13848
|
jointDef.set_localAnchorB(box2d.vec2dTo(localAnchorB));
|
|
13812
13849
|
jointDef.set_localAxisA(box2d.vec2dTo(localAxisA));
|
|
13813
|
-
jointDef.set_referenceAngle(
|
|
13850
|
+
jointDef.set_referenceAngle(objectB.body.GetAngle() - objectA.body.GetAngle());
|
|
13814
13851
|
jointDef.set_collideConnected(collide);
|
|
13815
13852
|
super(jointDef);
|
|
13816
13853
|
}
|
|
@@ -13913,7 +13950,7 @@ class Box2dWheelJoint extends Box2dJoint
|
|
|
13913
13950
|
anchor ||= box2d.vec2From(objectB.body.GetPosition());
|
|
13914
13951
|
const localAnchorA = objectA.worldToLocal(anchor);
|
|
13915
13952
|
const localAnchorB = objectB.worldToLocal(anchor);
|
|
13916
|
-
const localAxisA =
|
|
13953
|
+
const localAxisA = objectA.worldToLocalVector(worldAxis);
|
|
13917
13954
|
const jointDef = new box2d.instance.b2WheelJointDef();
|
|
13918
13955
|
jointDef.set_bodyA(objectA.body);
|
|
13919
13956
|
jointDef.set_bodyB(objectB.body);
|
|
@@ -14013,7 +14050,7 @@ class Box2dWeldJoint extends Box2dJoint
|
|
|
14013
14050
|
jointDef.set_bodyB(objectB.body);
|
|
14014
14051
|
jointDef.set_localAnchorA(box2d.vec2dTo(localAnchorA));
|
|
14015
14052
|
jointDef.set_localAnchorB(box2d.vec2dTo(localAnchorB));
|
|
14016
|
-
jointDef.set_referenceAngle(
|
|
14053
|
+
jointDef.set_referenceAngle(objectB.body.GetAngle() - objectA.body.GetAngle());
|
|
14017
14054
|
jointDef.set_collideConnected(collide);
|
|
14018
14055
|
super(jointDef);
|
|
14019
14056
|
}
|
|
@@ -14460,7 +14497,7 @@ class Box2dPlugin
|
|
|
14460
14497
|
* @param {number} [lineWidth]
|
|
14461
14498
|
* @param {boolean} [useWebGL=glEnable]
|
|
14462
14499
|
* @param {CanvasRenderingContext2D} [context] */
|
|
14463
|
-
drawFixture(fixture, pos, angle, color=WHITE, lineColor=BLACK, lineWidth=.1,
|
|
14500
|
+
drawFixture(fixture, pos, angle, color=WHITE, lineColor=BLACK, lineWidth=.1, useWebGL, context)
|
|
14464
14501
|
{
|
|
14465
14502
|
const shape = box2d.castShapeObject(fixture.GetShape());
|
|
14466
14503
|
switch (shape.GetType())
|
|
@@ -14470,20 +14507,20 @@ class Box2dPlugin
|
|
|
14470
14507
|
let points = [];
|
|
14471
14508
|
for (let i=shape.GetVertexCount(); i--;)
|
|
14472
14509
|
points.push(box2d.vec2From(shape.GetVertex(i)));
|
|
14473
|
-
drawPoly(points, color, lineWidth, lineColor, pos, angle,
|
|
14510
|
+
drawPoly(points, color, lineWidth, lineColor, pos, angle, useWebGL, false, context);
|
|
14474
14511
|
break;
|
|
14475
14512
|
}
|
|
14476
14513
|
case box2d.instance.b2Shape.e_circle:
|
|
14477
14514
|
{
|
|
14478
14515
|
const radius = shape.get_m_radius();
|
|
14479
|
-
drawCircle(pos, radius*2, color, lineWidth, lineColor,
|
|
14516
|
+
drawCircle(pos, radius*2, color, lineWidth, lineColor, useWebGL, false, context);
|
|
14480
14517
|
break;
|
|
14481
14518
|
}
|
|
14482
14519
|
case box2d.instance.b2Shape.e_edge:
|
|
14483
14520
|
{
|
|
14484
14521
|
const v1 = box2d.vec2From(shape.get_m_vertex1());
|
|
14485
14522
|
const v2 = box2d.vec2From(shape.get_m_vertex2());
|
|
14486
|
-
drawLine(v1, v2, lineWidth, lineColor, pos, angle,
|
|
14523
|
+
drawLine(v1, v2, lineWidth, lineColor, pos, angle, useWebGL, false, context);
|
|
14487
14524
|
break;
|
|
14488
14525
|
}
|
|
14489
14526
|
}
|
|
@@ -14888,6 +14925,444 @@ function getCrescentPoints(pos, size=1, percent=0, angle=0, invert=false, sides=
|
|
|
14888
14925
|
}
|
|
14889
14926
|
return points;
|
|
14890
14927
|
}
|
|
14928
|
+
/**
|
|
14929
|
+
* LittleJS Texture Sheet Plugin
|
|
14930
|
+
* - Packs images into texture sheets as they are loaded
|
|
14931
|
+
* - Sprites are placed automatically, callers get a TileInfo
|
|
14932
|
+
* - Sheets are created and filled as needed
|
|
14933
|
+
* - Sheets fill in call order, images decode in parallel
|
|
14934
|
+
* - Animation frames keep layout and wrap across rows as needed
|
|
14935
|
+
* - WebGL textures upload once per batch of loads
|
|
14936
|
+
* - loadAtlas imports pre-packed atlases (TexturePacker and Aseprite json)
|
|
14937
|
+
* @namespace TextureSheets
|
|
14938
|
+
*/
|
|
14939
|
+
|
|
14940
|
+
/** Width and height in pixels of texture sheets created by loadSprite
|
|
14941
|
+
* @type {number}
|
|
14942
|
+
* @default
|
|
14943
|
+
* @memberof Settings */
|
|
14944
|
+
let textureSheetSize = 2048;
|
|
14945
|
+
|
|
14946
|
+
/** Default padding pixels around each frame packed by loadSprite
|
|
14947
|
+
* @type {number}
|
|
14948
|
+
* @default
|
|
14949
|
+
* @memberof Settings */
|
|
14950
|
+
let textureSheetPadding = 1;
|
|
14951
|
+
|
|
14952
|
+
/** Array of texture sheets created by loadSprite
|
|
14953
|
+
* @type {Array<TextureSheet>}
|
|
14954
|
+
* @memberof TextureSheets */
|
|
14955
|
+
let textureSheets = [];
|
|
14956
|
+
|
|
14957
|
+
// pending loads pack through a queue so sheets fill in call order
|
|
14958
|
+
let textureSheetQueue = Promise.resolve();
|
|
14959
|
+
let textureSheetPendingCount = 0;
|
|
14960
|
+
|
|
14961
|
+
/**
|
|
14962
|
+
* Texture Sheet - A texture that images are packed into as they load
|
|
14963
|
+
* Uses shelf packing, images are placed left to right then wrap to a new row
|
|
14964
|
+
* @memberof TextureSheets
|
|
14965
|
+
*/
|
|
14966
|
+
class TextureSheet
|
|
14967
|
+
{
|
|
14968
|
+
/** Create a texture sheet, called automatically by loadSprite
|
|
14969
|
+
* @param {number} [size] - Width and height of the sheet in pixels */
|
|
14970
|
+
constructor(size=textureSheetSize)
|
|
14971
|
+
{
|
|
14972
|
+
ASSERT(size > 0, 'texture sheet size must be positive');
|
|
14973
|
+
|
|
14974
|
+
/** @property {number} - Width and height of the sheet in pixels */
|
|
14975
|
+
this.size = size;
|
|
14976
|
+
/** @property {OffscreenCanvas} - Canvas holding the packed images */
|
|
14977
|
+
this.canvas = headlessMode ? undefined : new OffscreenCanvas(size, size);
|
|
14978
|
+
/** @property {OffscreenCanvasRenderingContext2D} - 2d context for the canvas */
|
|
14979
|
+
this.context = this.canvas?.getContext('2d');
|
|
14980
|
+
/** @property {TextureInfo} - The texture info for this sheet */
|
|
14981
|
+
this.textureInfo = new TextureInfo(this.canvas);
|
|
14982
|
+
/** @property {Vector2} - Where the next image will be packed */
|
|
14983
|
+
this.cursor = vec2();
|
|
14984
|
+
/** @property {number} - Height of the row being packed */
|
|
14985
|
+
this.rowHeight = 0;
|
|
14986
|
+
/** @property {boolean} - Has the canvas changed since the last webgl upload? */
|
|
14987
|
+
this.glDirty = false;
|
|
14988
|
+
|
|
14989
|
+
if (headlessMode)
|
|
14990
|
+
{
|
|
14991
|
+
// tiles still need bounds when there is no canvas to measure
|
|
14992
|
+
this.textureInfo.size = vec2(size);
|
|
14993
|
+
this.textureInfo.sizeInverse = vec2(1/size);
|
|
14994
|
+
}
|
|
14995
|
+
}
|
|
14996
|
+
|
|
14997
|
+
/** Find a spot for an image on this sheet without drawing it
|
|
14998
|
+
* @param {Vector2} imageSize - Size of the source image in pixels
|
|
14999
|
+
* @param {Vector2} [frameSize] - Size of each frame, or the whole image if not passed
|
|
15000
|
+
* @param {number} [padding] - How many pixels padding around each frame
|
|
15001
|
+
* @return {TileInfo} Tile for the packed image, or undefined if the sheet is full */
|
|
15002
|
+
tryAdd(imageSize, frameSize=imageSize, padding=textureSheetPadding)
|
|
15003
|
+
{
|
|
15004
|
+
ASSERT(isVector2(imageSize) && isVector2(frameSize), 'sizes must be vec2');
|
|
15005
|
+
ASSERT(frameSize.x > 0 && frameSize.y > 0, 'frame size must be positive');
|
|
15006
|
+
ASSERT(imageSize.x % frameSize.x === 0 && imageSize.y % frameSize.y === 0,
|
|
15007
|
+
'image size must be a multiple of the frame size');
|
|
15008
|
+
|
|
15009
|
+
const cellWidth = frameSize.x + padding*2;
|
|
15010
|
+
const cellHeight = frameSize.y + padding*2;
|
|
15011
|
+
const maxColumns = this.size / cellWidth | 0;
|
|
15012
|
+
ASSERT(maxColumns > 0, 'frame is too wide to fit on a texture sheet');
|
|
15013
|
+
|
|
15014
|
+
// keep the layout of the source image, but narrow it if a row is too wide
|
|
15015
|
+
// frames wrap down to the next row, which TileInfo.frame handles via columns
|
|
15016
|
+
const sourceColumns = imageSize.x / frameSize.x;
|
|
15017
|
+
const frameCount = sourceColumns * (imageSize.y / frameSize.y);
|
|
15018
|
+
const columns = min(sourceColumns, maxColumns);
|
|
15019
|
+
const blockWidth = columns * cellWidth;
|
|
15020
|
+
const blockHeight = ceil(frameCount / columns) * cellHeight;
|
|
15021
|
+
|
|
15022
|
+
// probe the placement using locals so a failed try leaves the sheet unchanged
|
|
15023
|
+
let x = this.cursor.x, y = this.cursor.y, rowHeight = this.rowHeight;
|
|
15024
|
+
if (x + blockWidth > this.size)
|
|
15025
|
+
{
|
|
15026
|
+
// start a new row if this one does not have enough space left
|
|
15027
|
+
x = 0;
|
|
15028
|
+
y += rowHeight;
|
|
15029
|
+
rowHeight = 0;
|
|
15030
|
+
}
|
|
15031
|
+
|
|
15032
|
+
// out of space, the caller needs to use a different sheet
|
|
15033
|
+
if (y + blockHeight > this.size)
|
|
15034
|
+
return undefined;
|
|
15035
|
+
|
|
15036
|
+
// commit the placement, tile pos points inside the padding to match how tile() works
|
|
15037
|
+
this.cursor.x = x + blockWidth;
|
|
15038
|
+
this.cursor.y = y;
|
|
15039
|
+
this.rowHeight = max(rowHeight, blockHeight);
|
|
15040
|
+
return new TileInfo(vec2(x + padding, y + padding), frameSize, this.textureInfo, padding, 0, columns);
|
|
15041
|
+
}
|
|
15042
|
+
|
|
15043
|
+
/** Draw an image into this sheet at a tile returned by tryAdd
|
|
15044
|
+
* @param {HTMLImageElement} image - Source image to copy from
|
|
15045
|
+
* @param {TileInfo} tileInfo - Where to put it, from tryAdd
|
|
15046
|
+
* @param {boolean} [update] - Upload to webgl now, pass false when batching */
|
|
15047
|
+
drawImage(image, tileInfo, update=true)
|
|
15048
|
+
{
|
|
15049
|
+
ASSERT(!!this.context, 'texture sheet has no canvas');
|
|
15050
|
+
|
|
15051
|
+
// copy frames in order, reading the source left to right, top to bottom
|
|
15052
|
+
// the destination wraps at tileInfo.columns which may be narrower than the source
|
|
15053
|
+
const frameSize = tileInfo.size;
|
|
15054
|
+
const sourceColumns = image.width / frameSize.x;
|
|
15055
|
+
const frameCount = sourceColumns * (image.height / frameSize.y);
|
|
15056
|
+
const columns = tileInfo.columns || frameCount;
|
|
15057
|
+
const cellWidth = frameSize.x + tileInfo.padding*2;
|
|
15058
|
+
const cellHeight = frameSize.y + tileInfo.padding*2;
|
|
15059
|
+
for (let i = frameCount; i--;)
|
|
15060
|
+
{
|
|
15061
|
+
const sourceX = (i % sourceColumns) * frameSize.x;
|
|
15062
|
+
const sourceY = (i / sourceColumns | 0) * frameSize.y;
|
|
15063
|
+
this.context.drawImage(image,
|
|
15064
|
+
sourceX, sourceY, frameSize.x, frameSize.y,
|
|
15065
|
+
tileInfo.pos.x + (i % columns) * cellWidth,
|
|
15066
|
+
tileInfo.pos.y + (i / columns | 0) * cellHeight,
|
|
15067
|
+
frameSize.x, frameSize.y);
|
|
15068
|
+
}
|
|
15069
|
+
|
|
15070
|
+
// upload now unless the caller is batching more images
|
|
15071
|
+
this.glDirty = true;
|
|
15072
|
+
update && this.updateTexture();
|
|
15073
|
+
}
|
|
15074
|
+
|
|
15075
|
+
/** Upload the canvas to webgl if it has changed since the last upload
|
|
15076
|
+
* Only needed after batching, drawImage uploads automatically by default */
|
|
15077
|
+
updateTexture()
|
|
15078
|
+
{
|
|
15079
|
+
if (!this.glDirty) return;
|
|
15080
|
+
this.glDirty = false;
|
|
15081
|
+
this.textureInfo.createWebGLTexture();
|
|
15082
|
+
}
|
|
15083
|
+
}
|
|
15084
|
+
|
|
15085
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
15086
|
+
|
|
15087
|
+
/** Load an image and pack it into a texture sheet
|
|
15088
|
+
* - Returns a TileInfo immediately which is filled in when the image loads
|
|
15089
|
+
* - Nothing is visible until it loads, use spritesReady to wait for it
|
|
15090
|
+
* - Pass frameSize for animations, then step through them with TileInfo.frame
|
|
15091
|
+
* - Grid images keep their layout and frames wrap down to the next row
|
|
15092
|
+
* @param {string} src - Image source path
|
|
15093
|
+
* @param {Vector2|number} [frameSize] - Size of each animation frame in pixels
|
|
15094
|
+
* @param {number} [padding] - How many pixels padding around each frame
|
|
15095
|
+
* @return {TileInfo}
|
|
15096
|
+
* @example
|
|
15097
|
+
* const playerTile = loadSprite('player.png'); // a single sprite
|
|
15098
|
+
* const runTile = loadSprite('run.png', vec2(16)); // a 16x16 frame animation
|
|
15099
|
+
* @memberof TextureSheets */
|
|
15100
|
+
function loadSprite(src, frameSize, padding=textureSheetPadding)
|
|
15101
|
+
{
|
|
15102
|
+
ASSERT(isStringLike(src), 'image src must be a string');
|
|
15103
|
+
ASSERT(!frameSize || isVector2(frameSize) || isNumber(frameSize), 'frameSize must be a vec2 or number');
|
|
15104
|
+
ASSERT(isNumber(padding), 'padding must be a number');
|
|
15105
|
+
|
|
15106
|
+
if (isNumber(frameSize))
|
|
15107
|
+
frameSize = vec2(frameSize);
|
|
15108
|
+
|
|
15109
|
+
// start with an empty tile that gets filled in when the image loads
|
|
15110
|
+
const tileInfo = new TileInfo(vec2(), vec2(), undefined, padding, 0);
|
|
15111
|
+
if (headlessMode) return tileInfo;
|
|
15112
|
+
|
|
15113
|
+
// point at a sheet right away so drawing before it loads picks up empty pixels
|
|
15114
|
+
tileInfo.textureInfo = (textureSheets[0] || textureSheetCreate()).textureInfo;
|
|
15115
|
+
|
|
15116
|
+
// start decoding right away, images decode in parallel
|
|
15117
|
+
const image = new Image;
|
|
15118
|
+
const imagePromise = new Promise(resolve =>
|
|
15119
|
+
{
|
|
15120
|
+
image.onerror = image.onload = resolve;
|
|
15121
|
+
image.crossOrigin = 'anonymous';
|
|
15122
|
+
image.src = src;
|
|
15123
|
+
});
|
|
15124
|
+
|
|
15125
|
+
// pack through a queue so sheets fill in call order, not decode order
|
|
15126
|
+
++textureSheetPendingCount;
|
|
15127
|
+
textureSheetQueue = textureSheetQueue.then(async ()=>
|
|
15128
|
+
{
|
|
15129
|
+
await imagePromise;
|
|
15130
|
+
if (image.width)
|
|
15131
|
+
{
|
|
15132
|
+
// pack onto a sheet, then fill in the tile that was already handed out,
|
|
15133
|
+
// copying every field so nothing is missed if TileInfo gains more of them
|
|
15134
|
+
const imageSize = vec2(image.width, image.height);
|
|
15135
|
+
const {sheet, tile} = textureSheetAdd(imageSize, frameSize, padding);
|
|
15136
|
+
Object.assign(tileInfo, tile);
|
|
15137
|
+
sheet.drawImage(image, tileInfo, false); // upload once per batch below
|
|
15138
|
+
}
|
|
15139
|
+
else
|
|
15140
|
+
{
|
|
15141
|
+
// leave the tile empty if the image failed to load
|
|
15142
|
+
LOG('loadSprite failed to load image:', src);
|
|
15143
|
+
}
|
|
15144
|
+
|
|
15145
|
+
// upload to webgl once per batch, when the last pending load finishes
|
|
15146
|
+
if (!--textureSheetPendingCount)
|
|
15147
|
+
textureSheets.forEach(s=> s.updateTexture());
|
|
15148
|
+
});
|
|
15149
|
+
|
|
15150
|
+
return tileInfo;
|
|
15151
|
+
}
|
|
15152
|
+
|
|
15153
|
+
/** Load a pre-packed texture atlas and repack it onto texture sheets
|
|
15154
|
+
* - Supports TexturePacker json (hash and array) and Aseprite json
|
|
15155
|
+
* - Returns an empty object which is filled with TileInfos when loaded
|
|
15156
|
+
* - Frames are named by the json, animations are grouped automatically
|
|
15157
|
+
* - Aseprite frame tags become animations, so do names like run_0, run_1
|
|
15158
|
+
* - Trimmed frames are restored to their full source size when packed
|
|
15159
|
+
* - Rotated frames are rotated back upright when packed
|
|
15160
|
+
* @param {string} imageSrc - Atlas image path
|
|
15161
|
+
* @param {string|Object} jsonSrc - Atlas json path, or already parsed json data
|
|
15162
|
+
* @param {number} [padding] - How many pixels padding around each frame
|
|
15163
|
+
* @return {Object} Object mapping frame and animation names to TileInfos
|
|
15164
|
+
* @example
|
|
15165
|
+
* const atlas = loadAtlas('sprites.png', 'sprites.json');
|
|
15166
|
+
* await spritesReady();
|
|
15167
|
+
* drawTile(pos, size, atlas.player); // a single frame
|
|
15168
|
+
* drawTile(pos, size, atlas.run.frame(2)); // frame 2 of the run animation
|
|
15169
|
+
* @memberof TextureSheets */
|
|
15170
|
+
function loadAtlas(imageSrc, jsonSrc, padding=textureSheetPadding)
|
|
15171
|
+
{
|
|
15172
|
+
ASSERT(isStringLike(imageSrc), 'atlas image src must be a string');
|
|
15173
|
+
ASSERT(isStringLike(jsonSrc) || typeof jsonSrc === 'object', 'atlas json must be a path or object');
|
|
15174
|
+
ASSERT(isNumber(padding), 'padding must be a number');
|
|
15175
|
+
|
|
15176
|
+
const atlas = {};
|
|
15177
|
+
if (headlessMode) return atlas;
|
|
15178
|
+
|
|
15179
|
+
// start fetching the json and decoding the image right away, in parallel
|
|
15180
|
+
const jsonPromise = typeof jsonSrc === 'object' ? Promise.resolve(jsonSrc) :
|
|
15181
|
+
fetch(jsonSrc).then(r=> r.ok && r.json()).catch(()=> undefined);
|
|
15182
|
+
const image = new Image;
|
|
15183
|
+
const imagePromise = new Promise(resolve =>
|
|
15184
|
+
{
|
|
15185
|
+
image.onerror = image.onload = resolve;
|
|
15186
|
+
image.crossOrigin = 'anonymous';
|
|
15187
|
+
image.src = imageSrc;
|
|
15188
|
+
});
|
|
15189
|
+
|
|
15190
|
+
// pack through a queue so sheets fill in call order, not decode order
|
|
15191
|
+
++textureSheetPendingCount;
|
|
15192
|
+
textureSheetQueue = textureSheetQueue.then(async ()=>
|
|
15193
|
+
{
|
|
15194
|
+
const data = await jsonPromise;
|
|
15195
|
+
await imagePromise;
|
|
15196
|
+
if (image.width && data)
|
|
15197
|
+
{
|
|
15198
|
+
for (const group of parseAtlas(data))
|
|
15199
|
+
{
|
|
15200
|
+
// reserve a block of full size cells, one per frame
|
|
15201
|
+
const sourceSize = group.frames[0].sourceSize;
|
|
15202
|
+
const blockSize = vec2(sourceSize.x*group.frames.length, sourceSize.y);
|
|
15203
|
+
const {sheet, tile} = textureSheetAdd(blockSize, sourceSize, padding);
|
|
15204
|
+
|
|
15205
|
+
// draw each frame untrimmed into its cell
|
|
15206
|
+
const context = sheet.context;
|
|
15207
|
+
const cellWidth = sourceSize.x + padding*2;
|
|
15208
|
+
const cellHeight = sourceSize.y + padding*2;
|
|
15209
|
+
group.frames.forEach((f, i)=>
|
|
15210
|
+
{
|
|
15211
|
+
const x = tile.pos.x + (i % tile.columns)*cellWidth + f.offset.x;
|
|
15212
|
+
const y = tile.pos.y + (i / tile.columns |0)*cellHeight + f.offset.y;
|
|
15213
|
+
if (f.rotated)
|
|
15214
|
+
{
|
|
15215
|
+
// stored rotated 90 degrees clockwise, draw it back upright
|
|
15216
|
+
context.save();
|
|
15217
|
+
context.translate(x, y);
|
|
15218
|
+
context.rotate(-PI/2);
|
|
15219
|
+
context.drawImage(image, f.pos.x, f.pos.y, f.size.y, f.size.x,
|
|
15220
|
+
-f.size.y, 0, f.size.y, f.size.x);
|
|
15221
|
+
context.restore();
|
|
15222
|
+
}
|
|
15223
|
+
else
|
|
15224
|
+
context.drawImage(image, f.pos.x, f.pos.y, f.size.x, f.size.y,
|
|
15225
|
+
x, y, f.size.x, f.size.y);
|
|
15226
|
+
});
|
|
15227
|
+
sheet.glDirty = true;
|
|
15228
|
+
atlas[group.name] = tile;
|
|
15229
|
+
}
|
|
15230
|
+
}
|
|
15231
|
+
else
|
|
15232
|
+
{
|
|
15233
|
+
// leave the atlas empty if either file failed to load
|
|
15234
|
+
LOG('loadAtlas failed to load:', imageSrc, jsonSrc);
|
|
15235
|
+
}
|
|
15236
|
+
|
|
15237
|
+
// upload to webgl once per batch, when the last pending load finishes
|
|
15238
|
+
if (!--textureSheetPendingCount)
|
|
15239
|
+
textureSheets.forEach(s=> s.updateTexture());
|
|
15240
|
+
});
|
|
15241
|
+
|
|
15242
|
+
return atlas;
|
|
15243
|
+
}
|
|
15244
|
+
|
|
15245
|
+
/** Parse atlas json into a list of named frame groups, used by loadAtlas
|
|
15246
|
+
* - Accepts TexturePacker json (hash and array) and Aseprite json
|
|
15247
|
+
* - Frames tagged in Aseprite or named like run_0, run_1 group into animations
|
|
15248
|
+
* @param {Object} data - Parsed atlas json data
|
|
15249
|
+
* @return {Array<Object>} List of {name, frames} groups in atlas order
|
|
15250
|
+
* @memberof TextureSheets */
|
|
15251
|
+
function parseAtlas(data)
|
|
15252
|
+
{
|
|
15253
|
+
ASSERT(!!data?.frames, 'unrecognized atlas format, expected TexturePacker or Aseprite json');
|
|
15254
|
+
|
|
15255
|
+
// normalize both hash and array frame layouts into a single list
|
|
15256
|
+
const frames = (isArray(data.frames) ?
|
|
15257
|
+
data.frames.map(f=> [f.filename, f]) : Object.entries(data.frames))
|
|
15258
|
+
.map(([name, f])=> ({
|
|
15259
|
+
name: name.replace(/\.[^.\\/]+$/, ''), // strip file extension
|
|
15260
|
+
pos: vec2(f.frame.x, f.frame.y),
|
|
15261
|
+
size: vec2(f.frame.w, f.frame.h),
|
|
15262
|
+
offset: vec2(f.spriteSourceSize?.x ?? 0, f.spriteSourceSize?.y ?? 0),
|
|
15263
|
+
sourceSize: vec2(f.sourceSize?.w ?? f.frame.w, f.sourceSize?.h ?? f.frame.h),
|
|
15264
|
+
rotated: !!f.rotated,
|
|
15265
|
+
}));
|
|
15266
|
+
|
|
15267
|
+
const groups = [];
|
|
15268
|
+
const tags = data.meta?.frameTags;
|
|
15269
|
+
if (tags?.length)
|
|
15270
|
+
{
|
|
15271
|
+
// aseprite tags are authoritative, untagged frames stay individual
|
|
15272
|
+
const tagged = new Set;
|
|
15273
|
+
for (const tag of tags)
|
|
15274
|
+
{
|
|
15275
|
+
groups.push({name: tag.name, frames: frames.slice(tag.from, tag.to + 1)});
|
|
15276
|
+
for (let i = tag.from; i <= tag.to; ++i)
|
|
15277
|
+
tagged.add(i);
|
|
15278
|
+
}
|
|
15279
|
+
frames.forEach((f, i)=> tagged.has(i) || groups.push({name: f.name, frames: [f]}));
|
|
15280
|
+
return groups;
|
|
15281
|
+
}
|
|
15282
|
+
|
|
15283
|
+
// group frames that share a name stem with contiguous trailing numbers
|
|
15284
|
+
// run_0.png and run_1.png become a 2 frame animation named run
|
|
15285
|
+
const stems = new Map;
|
|
15286
|
+
for (const f of frames)
|
|
15287
|
+
{
|
|
15288
|
+
let match = f.name.match(/^(.+?)([-_ ])?(\d+)$/);
|
|
15289
|
+
if (match && !match[2] && /\d$/.test(match[1]))
|
|
15290
|
+
match = undefined; // all digit tails like 10 are a name, not frame 0 of 1
|
|
15291
|
+
const stem = match ? match[1] : f.name;
|
|
15292
|
+
f.groupIndex = match ? Number(match[3]) : undefined;
|
|
15293
|
+
stems.has(stem) || stems.set(stem, []);
|
|
15294
|
+
stems.get(stem).push(f);
|
|
15295
|
+
}
|
|
15296
|
+
for (const [stem, list] of stems)
|
|
15297
|
+
{
|
|
15298
|
+
// only group 2 or more frames with contiguous indices and matching sizes
|
|
15299
|
+
list.sort((a, b)=> a.groupIndex - b.groupIndex);
|
|
15300
|
+
const grouped = list.length > 1 &&
|
|
15301
|
+
list.every((f, i)=> f.groupIndex === list[0].groupIndex + i) &&
|
|
15302
|
+
list.every(f=> f.sourceSize.x === list[0].sourceSize.x &&
|
|
15303
|
+
f.sourceSize.y === list[0].sourceSize.y);
|
|
15304
|
+
if (grouped)
|
|
15305
|
+
groups.push({name: stem, frames: list});
|
|
15306
|
+
else
|
|
15307
|
+
list.forEach(f=> groups.push({name: f.name, frames: [f]}));
|
|
15308
|
+
}
|
|
15309
|
+
return groups;
|
|
15310
|
+
}
|
|
15311
|
+
|
|
15312
|
+
/** Wait for everything started by loadSprite and loadAtlas to finish packing
|
|
15313
|
+
* @return {Promise}
|
|
15314
|
+
* @example
|
|
15315
|
+
* async function gameInit()
|
|
15316
|
+
* {
|
|
15317
|
+
* playerTile = loadSprite('player.png');
|
|
15318
|
+
* runTile = loadSprite('run.png', vec2(16));
|
|
15319
|
+
* await spritesReady();
|
|
15320
|
+
* }
|
|
15321
|
+
* @memberof TextureSheets */
|
|
15322
|
+
async function spritesReady()
|
|
15323
|
+
{
|
|
15324
|
+
// keep waiting until the queue drains, more sprites may load while waiting
|
|
15325
|
+
while (textureSheetPendingCount)
|
|
15326
|
+
await textureSheetQueue;
|
|
15327
|
+
}
|
|
15328
|
+
|
|
15329
|
+
// create a new texture sheet and add it to the list
|
|
15330
|
+
function textureSheetCreate()
|
|
15331
|
+
{
|
|
15332
|
+
const sheet = new TextureSheet;
|
|
15333
|
+
textureSheets.push(sheet);
|
|
15334
|
+
return sheet;
|
|
15335
|
+
}
|
|
15336
|
+
|
|
15337
|
+
// use the first sheet with enough space, or make a new one
|
|
15338
|
+
function textureSheetAdd(imageSize, frameSize, padding)
|
|
15339
|
+
{
|
|
15340
|
+
let sheet, tile;
|
|
15341
|
+
for (sheet of textureSheets)
|
|
15342
|
+
if (tile = sheet.tryAdd(imageSize, frameSize, padding))
|
|
15343
|
+
break;
|
|
15344
|
+
if (!tile)
|
|
15345
|
+
{
|
|
15346
|
+
sheet = textureSheetCreate();
|
|
15347
|
+
tile = sheet.tryAdd(imageSize, frameSize, padding);
|
|
15348
|
+
ASSERT(!!tile, 'image is too large to fit on a texture sheet');
|
|
15349
|
+
}
|
|
15350
|
+
return {sheet, tile};
|
|
15351
|
+
}
|
|
15352
|
+
|
|
15353
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
15354
|
+
// Texture sheet setting setters
|
|
15355
|
+
|
|
15356
|
+
/** Set width and height in pixels of texture sheets created by loadSprite
|
|
15357
|
+
* @param {number} size
|
|
15358
|
+
* @memberof Settings */
|
|
15359
|
+
function setTextureSheetSize(size) { textureSheetSize = size; }
|
|
15360
|
+
|
|
15361
|
+
/** Set default padding pixels around each frame packed by loadSprite
|
|
15362
|
+
* @param {number} padding
|
|
15363
|
+
* @memberof Settings */
|
|
15364
|
+
function setTextureSheetPadding(padding) { textureSheetPadding = padding; }
|
|
15365
|
+
|
|
14891
15366
|
/**
|
|
14892
15367
|
* LittleJS Tween System Plugin
|
|
14893
15368
|
* - Lightweight tweens for numbers, Vector2, Color, or any .lerp-able type
|
|
@@ -15314,7 +15789,11 @@ function tweenProperty(target, propertyPath, start, end, duration = 1, options =
|
|
|
15314
15789
|
const callback = (value) =>
|
|
15315
15790
|
{
|
|
15316
15791
|
let obj = target;
|
|
15317
|
-
for (const k of parts)
|
|
15792
|
+
for (const k of parts)
|
|
15793
|
+
{
|
|
15794
|
+
obj = obj[k];
|
|
15795
|
+
ASSERT(obj != null, 'tweenProperty path does not resolve: ' + propertyPath);
|
|
15796
|
+
}
|
|
15318
15797
|
obj[lastKey] = value;
|
|
15319
15798
|
};
|
|
15320
15799
|
return new Tween(callback, start, end, duration, options);
|
|
@@ -16197,6 +16676,160 @@ class PathFinder
|
|
|
16197
16676
|
}
|
|
16198
16677
|
}
|
|
16199
16678
|
|
|
16679
|
+
/**
|
|
16680
|
+
* LittleJS Three.js Plugin
|
|
16681
|
+
* - Renders a three.js scene on a canvas behind the LittleJS canvases
|
|
16682
|
+
* - The three.js module is passed in by the user, nothing is bundled
|
|
16683
|
+
* - Keep canvasClearColor transparent so the 3D scene shows through
|
|
16684
|
+
* - Aligned camera mode locks the 3D camera to the LittleJS 2D camera
|
|
16685
|
+
* - ThreeJSObject lets LittleJS physics drive a three.js mesh
|
|
16686
|
+
* - Call new ThreeJSPlugin(THREE) in gameInit to set up
|
|
16687
|
+
* @namespace ThreeJS
|
|
16688
|
+
*/
|
|
16689
|
+
|
|
16690
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
16691
|
+
|
|
16692
|
+
/** Global ThreeJS plugin object
|
|
16693
|
+
* @type {ThreeJSPlugin}
|
|
16694
|
+
* @memberof ThreeJS */
|
|
16695
|
+
let threeJS;
|
|
16696
|
+
|
|
16697
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
16698
|
+
/**
|
|
16699
|
+
* ThreeJS Plugin - Renders a three.js scene behind the LittleJS canvas
|
|
16700
|
+
* @example
|
|
16701
|
+
* // in gameInit, with three.js loaded by the user
|
|
16702
|
+
* new ThreeJSPlugin(THREE);
|
|
16703
|
+
* threeJS.scene.add(new THREE.AmbientLight);
|
|
16704
|
+
* @memberof ThreeJS
|
|
16705
|
+
*/
|
|
16706
|
+
class ThreeJSPlugin
|
|
16707
|
+
{
|
|
16708
|
+
/** Set up the three.js rendering layer, call in gameInit
|
|
16709
|
+
* @param {Object} THREE - The three.js module, supplied by the user
|
|
16710
|
+
* @param {number} [cameraFOV] - Vertical field of view in degrees */
|
|
16711
|
+
constructor(THREE, cameraFOV=60)
|
|
16712
|
+
{
|
|
16713
|
+
ASSERT(!threeJS, 'ThreeJS plugin already initialized');
|
|
16714
|
+
threeJS = this;
|
|
16715
|
+
if (headlessMode) return;
|
|
16716
|
+
ASSERT(mainCanvas, 'ThreeJS plugin must be created after engineInit, call in gameInit');
|
|
16717
|
+
ASSERT(THREE && THREE.WebGLRenderer, 'three.js module must be passed in');
|
|
16718
|
+
|
|
16719
|
+
/** @property {Object} - The three.js module passed into the constructor */
|
|
16720
|
+
this.THREE = THREE;
|
|
16721
|
+
/** @property {Object} - The three.js renderer */
|
|
16722
|
+
this.renderer = new THREE.WebGLRenderer({antialias: true});
|
|
16723
|
+
/** @property {Object} - The three.js scene, add lights and meshes here */
|
|
16724
|
+
this.scene = new THREE.Scene();
|
|
16725
|
+
/** @property {Object} - The three.js perspective camera */
|
|
16726
|
+
this.camera = new THREE.PerspectiveCamera(cameraFOV, 1, .1, 1e3);
|
|
16727
|
+
/** @property {boolean} - Lock the camera to the LittleJS 2D camera so the z=0 plane matches world space */
|
|
16728
|
+
this.cameraAlign2D = true;
|
|
16729
|
+
|
|
16730
|
+
// insert the canvas below the engine canvases and match the layout
|
|
16731
|
+
const threeCanvas = this.renderer.domElement;
|
|
16732
|
+
const rootElement = mainCanvas.parentElement;
|
|
16733
|
+
rootElement.insertBefore(threeCanvas, rootElement.firstChild);
|
|
16734
|
+
threeCanvas.style.cssText = mainCanvas.style.cssText;
|
|
16735
|
+
|
|
16736
|
+
// render automatically each frame after the engine renders
|
|
16737
|
+
engineAddPlugin(undefined, ()=> this.render());
|
|
16738
|
+
}
|
|
16739
|
+
|
|
16740
|
+
/** Position the camera so the z=0 plane exactly matches LittleJS world space,
|
|
16741
|
+
* called automatically when cameraAlign2D is set */
|
|
16742
|
+
alignCamera2D()
|
|
16743
|
+
{
|
|
16744
|
+
const halfHeight = mainCanvasSize.y / 2 / cameraScale; // half visible height in world units
|
|
16745
|
+
const distance = halfHeight / tan(this.camera.fov/2 * PI/180);
|
|
16746
|
+
this.camera.position.set(cameraPos.x, cameraPos.y, distance);
|
|
16747
|
+
// reset all axes in case a free camera was used, littlejs angles are clockwise
|
|
16748
|
+
this.camera.rotation.set(0, 0, -cameraAngle);
|
|
16749
|
+
}
|
|
16750
|
+
|
|
16751
|
+
/** Sync the canvas layout and render the scene, called automatically each frame */
|
|
16752
|
+
render()
|
|
16753
|
+
{
|
|
16754
|
+
if (!this.renderer) return; // headless mode
|
|
16755
|
+
|
|
16756
|
+
// keep renderer size and css in sync with the LittleJS canvas
|
|
16757
|
+
const threeCanvas = this.renderer.domElement;
|
|
16758
|
+
if (threeCanvas.width != mainCanvasSize.x || threeCanvas.height != mainCanvasSize.y)
|
|
16759
|
+
{
|
|
16760
|
+
this.renderer.setSize(mainCanvasSize.x, mainCanvasSize.y, false);
|
|
16761
|
+
this.camera.aspect = mainCanvasSize.x / mainCanvasSize.y;
|
|
16762
|
+
this.camera.updateProjectionMatrix();
|
|
16763
|
+
}
|
|
16764
|
+
if (threeCanvas.style.cssText != mainCanvas.style.cssText)
|
|
16765
|
+
threeCanvas.style.cssText = mainCanvas.style.cssText;
|
|
16766
|
+
|
|
16767
|
+
if (this.cameraAlign2D)
|
|
16768
|
+
this.alignCamera2D();
|
|
16769
|
+
this.renderer.render(this.scene, this.camera);
|
|
16770
|
+
}
|
|
16771
|
+
}
|
|
16772
|
+
|
|
16773
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
16774
|
+
/**
|
|
16775
|
+
* ThreeJS Object - EngineObject that drives a three.js mesh
|
|
16776
|
+
* - LittleJS physics moves the object and the mesh follows automatically
|
|
16777
|
+
* - Destroying the object removes the mesh from the scene
|
|
16778
|
+
* @extends EngineObject
|
|
16779
|
+
* @memberof ThreeJS
|
|
16780
|
+
*/
|
|
16781
|
+
class ThreeJSObject extends EngineObject
|
|
16782
|
+
{
|
|
16783
|
+
/** Create an engine object that drives a three.js mesh
|
|
16784
|
+
* @param {Vector2} [pos] - World space position
|
|
16785
|
+
* @param {Vector2} [size] - World space size
|
|
16786
|
+
* @param {Object} [mesh] - The three.js object3d to drive
|
|
16787
|
+
* @param {number} [z] - Mesh height above the 2D plane */
|
|
16788
|
+
constructor(pos, size, mesh, z=0)
|
|
16789
|
+
{
|
|
16790
|
+
super(pos, size);
|
|
16791
|
+
ASSERT(threeJS, 'ThreeJS plugin must be initialized first');
|
|
16792
|
+
|
|
16793
|
+
/** @property {Object} - The three.js object3d this object drives */
|
|
16794
|
+
this.mesh = mesh;
|
|
16795
|
+
/** @property {number} - Mesh height above the 2D plane */
|
|
16796
|
+
this.z = z;
|
|
16797
|
+
if (mesh)
|
|
16798
|
+
{
|
|
16799
|
+
threeJS.scene.add(mesh);
|
|
16800
|
+
this.syncMesh();
|
|
16801
|
+
}
|
|
16802
|
+
}
|
|
16803
|
+
|
|
16804
|
+
/** Update the object and sync the mesh to its transform */
|
|
16805
|
+
update()
|
|
16806
|
+
{
|
|
16807
|
+
super.update();
|
|
16808
|
+
this.syncMesh();
|
|
16809
|
+
}
|
|
16810
|
+
|
|
16811
|
+
/** Copy this object's transform to the mesh */
|
|
16812
|
+
syncMesh()
|
|
16813
|
+
{
|
|
16814
|
+
if (!this.mesh) return;
|
|
16815
|
+
this.mesh.position.set(this.pos.x, this.pos.y, this.z);
|
|
16816
|
+
this.mesh.rotation.z = -this.angle; // littlejs angles are clockwise
|
|
16817
|
+
}
|
|
16818
|
+
|
|
16819
|
+
/** The mesh is this object's visual, the default 2D rendering is skipped */
|
|
16820
|
+
render() {}
|
|
16821
|
+
|
|
16822
|
+
/** Destroy this object and remove its mesh from the scene
|
|
16823
|
+
* @param {boolean} [immediate] */
|
|
16824
|
+
destroy(immediate)
|
|
16825
|
+
{
|
|
16826
|
+
if (this.destroyed) return;
|
|
16827
|
+
// note: frequently destroyed objects should also dispose geometry and material
|
|
16828
|
+
this.mesh && threeJS.scene.remove(this.mesh);
|
|
16829
|
+
super.destroy(immediate);
|
|
16830
|
+
}
|
|
16831
|
+
}
|
|
16832
|
+
|
|
16200
16833
|
|
|
16201
16834
|
/**
|
|
16202
16835
|
* LittleJS Module Export
|
|
@@ -16683,4 +17316,22 @@ export
|
|
|
16683
17316
|
// Path Finding
|
|
16684
17317
|
PathFinder,
|
|
16685
17318
|
PathFinderNode,
|
|
16686
|
-
|
|
17319
|
+
|
|
17320
|
+
// Three.js
|
|
17321
|
+
threeJS,
|
|
17322
|
+
ThreeJSPlugin,
|
|
17323
|
+
ThreeJSObject,
|
|
17324
|
+
|
|
17325
|
+
// Texture Sheets
|
|
17326
|
+
textureSheetSize,
|
|
17327
|
+
textureSheetPadding,
|
|
17328
|
+
setTextureSheetSize,
|
|
17329
|
+
setTextureSheetPadding,
|
|
17330
|
+
textureSheets,
|
|
17331
|
+
TextureSheet,
|
|
17332
|
+
loadSprite,
|
|
17333
|
+
loadAtlas,
|
|
17334
|
+
parseAtlas,
|
|
17335
|
+
spritesReady,
|
|
17336
|
+
}
|
|
17337
|
+
|