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/package.json
CHANGED
package/plugins/box2d.js
CHANGED
|
@@ -345,7 +345,7 @@ class Box2dObject extends EngineObject
|
|
|
345
345
|
shape.set_m_vertex3(box2d.vec2dTo(getPoint(i+2)));
|
|
346
346
|
const f = this.addShape(shape, density, friction, restitution, isSensor);
|
|
347
347
|
fixtures.push(f);
|
|
348
|
-
|
|
348
|
+
edgePoints.push(points[i].copy());
|
|
349
349
|
}
|
|
350
350
|
this.edgeLoops.push(edgePoints);
|
|
351
351
|
return fixtures;
|
|
@@ -411,7 +411,7 @@ class Box2dObject extends EngineObject
|
|
|
411
411
|
/** Sets the position
|
|
412
412
|
* @param {Vector2} pos */
|
|
413
413
|
setPosition(pos)
|
|
414
|
-
{ this.setTransform(pos, this.body.GetAngle()); }
|
|
414
|
+
{ this.setTransform(pos, -this.body.GetAngle()); }
|
|
415
415
|
|
|
416
416
|
/** Sets the angle
|
|
417
417
|
* @param {number} angle */
|
|
@@ -508,6 +508,7 @@ class Box2dObject extends EngineObject
|
|
|
508
508
|
filter.set_categoryBits(categoryBits);
|
|
509
509
|
filter.set_maskBits(0xffff & ~ignoreCategoryBits);
|
|
510
510
|
filter.set_groupIndex(groupIndex);
|
|
511
|
+
fixture.SetFilterData(filter); // applies and refilters contacts
|
|
511
512
|
});
|
|
512
513
|
}
|
|
513
514
|
|
|
@@ -1043,7 +1044,7 @@ class Box2dRevoluteJoint extends Box2dJoint
|
|
|
1043
1044
|
jointDef.set_bodyB(objectB.body);
|
|
1044
1045
|
jointDef.set_localAnchorA(box2d.vec2dTo(localAnchorA));
|
|
1045
1046
|
jointDef.set_localAnchorB(box2d.vec2dTo(localAnchorB));
|
|
1046
|
-
jointDef.set_referenceAngle(
|
|
1047
|
+
jointDef.set_referenceAngle(objectB.body.GetAngle() - objectA.body.GetAngle());
|
|
1047
1048
|
jointDef.set_collideConnected(collide);
|
|
1048
1049
|
super(jointDef);
|
|
1049
1050
|
}
|
|
@@ -1190,14 +1191,14 @@ class Box2dPrismaticJoint extends Box2dJoint
|
|
|
1190
1191
|
anchor ||= box2d.vec2From(objectB.body.GetPosition());
|
|
1191
1192
|
const localAnchorA = objectA.worldToLocal(anchor);
|
|
1192
1193
|
const localAnchorB = objectB.worldToLocal(anchor);
|
|
1193
|
-
const localAxisA =
|
|
1194
|
+
const localAxisA = objectA.worldToLocalVector(worldAxis);
|
|
1194
1195
|
const jointDef = new box2d.instance.b2PrismaticJointDef();
|
|
1195
1196
|
jointDef.set_bodyA(objectA.body);
|
|
1196
1197
|
jointDef.set_bodyB(objectB.body);
|
|
1197
1198
|
jointDef.set_localAnchorA(box2d.vec2dTo(localAnchorA));
|
|
1198
1199
|
jointDef.set_localAnchorB(box2d.vec2dTo(localAnchorB));
|
|
1199
1200
|
jointDef.set_localAxisA(box2d.vec2dTo(localAxisA));
|
|
1200
|
-
jointDef.set_referenceAngle(
|
|
1201
|
+
jointDef.set_referenceAngle(objectB.body.GetAngle() - objectA.body.GetAngle());
|
|
1201
1202
|
jointDef.set_collideConnected(collide);
|
|
1202
1203
|
super(jointDef);
|
|
1203
1204
|
}
|
|
@@ -1300,7 +1301,7 @@ class Box2dWheelJoint extends Box2dJoint
|
|
|
1300
1301
|
anchor ||= box2d.vec2From(objectB.body.GetPosition());
|
|
1301
1302
|
const localAnchorA = objectA.worldToLocal(anchor);
|
|
1302
1303
|
const localAnchorB = objectB.worldToLocal(anchor);
|
|
1303
|
-
const localAxisA =
|
|
1304
|
+
const localAxisA = objectA.worldToLocalVector(worldAxis);
|
|
1304
1305
|
const jointDef = new box2d.instance.b2WheelJointDef();
|
|
1305
1306
|
jointDef.set_bodyA(objectA.body);
|
|
1306
1307
|
jointDef.set_bodyB(objectB.body);
|
|
@@ -1400,7 +1401,7 @@ class Box2dWeldJoint extends Box2dJoint
|
|
|
1400
1401
|
jointDef.set_bodyB(objectB.body);
|
|
1401
1402
|
jointDef.set_localAnchorA(box2d.vec2dTo(localAnchorA));
|
|
1402
1403
|
jointDef.set_localAnchorB(box2d.vec2dTo(localAnchorB));
|
|
1403
|
-
jointDef.set_referenceAngle(
|
|
1404
|
+
jointDef.set_referenceAngle(objectB.body.GetAngle() - objectA.body.GetAngle());
|
|
1404
1405
|
jointDef.set_collideConnected(collide);
|
|
1405
1406
|
super(jointDef);
|
|
1406
1407
|
}
|
|
@@ -1847,7 +1848,7 @@ class Box2dPlugin
|
|
|
1847
1848
|
* @param {number} [lineWidth]
|
|
1848
1849
|
* @param {boolean} [useWebGL=glEnable]
|
|
1849
1850
|
* @param {CanvasRenderingContext2D} [context] */
|
|
1850
|
-
drawFixture(fixture, pos, angle, color=WHITE, lineColor=BLACK, lineWidth=.1,
|
|
1851
|
+
drawFixture(fixture, pos, angle, color=WHITE, lineColor=BLACK, lineWidth=.1, useWebGL, context)
|
|
1851
1852
|
{
|
|
1852
1853
|
const shape = box2d.castShapeObject(fixture.GetShape());
|
|
1853
1854
|
switch (shape.GetType())
|
|
@@ -1857,20 +1858,20 @@ class Box2dPlugin
|
|
|
1857
1858
|
let points = [];
|
|
1858
1859
|
for (let i=shape.GetVertexCount(); i--;)
|
|
1859
1860
|
points.push(box2d.vec2From(shape.GetVertex(i)));
|
|
1860
|
-
drawPoly(points, color, lineWidth, lineColor, pos, angle,
|
|
1861
|
+
drawPoly(points, color, lineWidth, lineColor, pos, angle, useWebGL, false, context);
|
|
1861
1862
|
break;
|
|
1862
1863
|
}
|
|
1863
1864
|
case box2d.instance.b2Shape.e_circle:
|
|
1864
1865
|
{
|
|
1865
1866
|
const radius = shape.get_m_radius();
|
|
1866
|
-
drawCircle(pos, radius*2, color, lineWidth, lineColor,
|
|
1867
|
+
drawCircle(pos, radius*2, color, lineWidth, lineColor, useWebGL, false, context);
|
|
1867
1868
|
break;
|
|
1868
1869
|
}
|
|
1869
1870
|
case box2d.instance.b2Shape.e_edge:
|
|
1870
1871
|
{
|
|
1871
1872
|
const v1 = box2d.vec2From(shape.get_m_vertex1());
|
|
1872
1873
|
const v2 = box2d.vec2From(shape.get_m_vertex2());
|
|
1873
|
-
drawLine(v1, v2, lineWidth, lineColor, pos, angle,
|
|
1874
|
+
drawLine(v1, v2, lineWidth, lineColor, pos, angle, useWebGL, false, context);
|
|
1874
1875
|
break;
|
|
1875
1876
|
}
|
|
1876
1877
|
}
|
package/plugins/newgrounds.js
CHANGED
|
@@ -193,6 +193,7 @@ class NewgroundsPlugin
|
|
|
193
193
|
return;
|
|
194
194
|
}
|
|
195
195
|
debugMedals && LOG(xmlHttp.responseText);
|
|
196
|
-
return xmlHttp.responseText && JSON.parse(xmlHttp.responseText);
|
|
196
|
+
try { return xmlHttp.responseText && JSON.parse(xmlHttp.responseText); }
|
|
197
|
+
catch(e) { debugMedals && LOG('newgrounds response is not valid JSON', e); }
|
|
197
198
|
}
|
|
198
199
|
}
|
package/plugins/pluginExport.js
CHANGED
|
@@ -95,4 +95,21 @@ export
|
|
|
95
95
|
// Path Finding
|
|
96
96
|
PathFinder,
|
|
97
97
|
PathFinderNode,
|
|
98
|
-
|
|
98
|
+
|
|
99
|
+
// Three.js
|
|
100
|
+
threeJS,
|
|
101
|
+
ThreeJSPlugin,
|
|
102
|
+
ThreeJSObject,
|
|
103
|
+
|
|
104
|
+
// Texture Sheets
|
|
105
|
+
textureSheetSize,
|
|
106
|
+
textureSheetPadding,
|
|
107
|
+
setTextureSheetSize,
|
|
108
|
+
setTextureSheetPadding,
|
|
109
|
+
textureSheets,
|
|
110
|
+
TextureSheet,
|
|
111
|
+
loadSprite,
|
|
112
|
+
loadAtlas,
|
|
113
|
+
parseAtlas,
|
|
114
|
+
spritesReady,
|
|
115
|
+
}
|
package/plugins/postProcess.js
CHANGED
|
@@ -16,8 +16,8 @@
|
|
|
16
16
|
let postProcess;
|
|
17
17
|
|
|
18
18
|
/////////////////////////////////////////////////////////////////////////
|
|
19
|
-
/**
|
|
20
|
-
*
|
|
19
|
+
/**
|
|
20
|
+
* Post Process Plugin - Applies a full screen shader to the rendered output
|
|
21
21
|
* @memberof PostProcess
|
|
22
22
|
*/
|
|
23
23
|
class PostProcessPlugin
|
|
@@ -131,8 +131,9 @@ class PostProcessPlugin
|
|
|
131
131
|
workCanvas.height = mainCanvasSize.y;
|
|
132
132
|
glCopyToContext(workContext);
|
|
133
133
|
workContext.drawImage(mainCanvas, 0, 0);
|
|
134
|
-
mainCanvas.width |= 0
|
|
135
|
-
|
|
134
|
+
mainCanvas.width |= 0; // setting size clears the main canvas
|
|
135
|
+
|
|
136
|
+
|
|
136
137
|
// copy work canvas to texture
|
|
137
138
|
glContext.texImage2D(glContext.TEXTURE_2D, 0, glContext.RGBA, glContext.RGBA, glContext.UNSIGNED_BYTE, workCanvas);
|
|
138
139
|
}
|
|
@@ -0,0 +1,439 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LittleJS Texture Sheet Plugin
|
|
3
|
+
* - Packs images into texture sheets as they are loaded
|
|
4
|
+
* - Sprites are placed automatically, callers get a TileInfo
|
|
5
|
+
* - Sheets are created and filled as needed
|
|
6
|
+
* - Sheets fill in call order, images decode in parallel
|
|
7
|
+
* - Animation frames keep layout and wrap across rows as needed
|
|
8
|
+
* - WebGL textures upload once per batch of loads
|
|
9
|
+
* - loadAtlas imports pre-packed atlases (TexturePacker and Aseprite json)
|
|
10
|
+
* @namespace TextureSheets
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
'use strict';
|
|
14
|
+
|
|
15
|
+
/** Width and height in pixels of texture sheets created by loadSprite
|
|
16
|
+
* @type {number}
|
|
17
|
+
* @default
|
|
18
|
+
* @memberof Settings */
|
|
19
|
+
let textureSheetSize = 2048;
|
|
20
|
+
|
|
21
|
+
/** Default padding pixels around each frame packed by loadSprite
|
|
22
|
+
* @type {number}
|
|
23
|
+
* @default
|
|
24
|
+
* @memberof Settings */
|
|
25
|
+
let textureSheetPadding = 1;
|
|
26
|
+
|
|
27
|
+
/** Array of texture sheets created by loadSprite
|
|
28
|
+
* @type {Array<TextureSheet>}
|
|
29
|
+
* @memberof TextureSheets */
|
|
30
|
+
let textureSheets = [];
|
|
31
|
+
|
|
32
|
+
// pending loads pack through a queue so sheets fill in call order
|
|
33
|
+
let textureSheetQueue = Promise.resolve();
|
|
34
|
+
let textureSheetPendingCount = 0;
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Texture Sheet - A texture that images are packed into as they load
|
|
38
|
+
* Uses shelf packing, images are placed left to right then wrap to a new row
|
|
39
|
+
* @memberof TextureSheets
|
|
40
|
+
*/
|
|
41
|
+
class TextureSheet
|
|
42
|
+
{
|
|
43
|
+
/** Create a texture sheet, called automatically by loadSprite
|
|
44
|
+
* @param {number} [size] - Width and height of the sheet in pixels */
|
|
45
|
+
constructor(size=textureSheetSize)
|
|
46
|
+
{
|
|
47
|
+
ASSERT(size > 0, 'texture sheet size must be positive');
|
|
48
|
+
|
|
49
|
+
/** @property {number} - Width and height of the sheet in pixels */
|
|
50
|
+
this.size = size;
|
|
51
|
+
/** @property {OffscreenCanvas} - Canvas holding the packed images */
|
|
52
|
+
this.canvas = headlessMode ? undefined : new OffscreenCanvas(size, size);
|
|
53
|
+
/** @property {OffscreenCanvasRenderingContext2D} - 2d context for the canvas */
|
|
54
|
+
this.context = this.canvas?.getContext('2d');
|
|
55
|
+
/** @property {TextureInfo} - The texture info for this sheet */
|
|
56
|
+
this.textureInfo = new TextureInfo(this.canvas);
|
|
57
|
+
/** @property {Vector2} - Where the next image will be packed */
|
|
58
|
+
this.cursor = vec2();
|
|
59
|
+
/** @property {number} - Height of the row being packed */
|
|
60
|
+
this.rowHeight = 0;
|
|
61
|
+
/** @property {boolean} - Has the canvas changed since the last webgl upload? */
|
|
62
|
+
this.glDirty = false;
|
|
63
|
+
|
|
64
|
+
if (headlessMode)
|
|
65
|
+
{
|
|
66
|
+
// tiles still need bounds when there is no canvas to measure
|
|
67
|
+
this.textureInfo.size = vec2(size);
|
|
68
|
+
this.textureInfo.sizeInverse = vec2(1/size);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Find a spot for an image on this sheet without drawing it
|
|
73
|
+
* @param {Vector2} imageSize - Size of the source image in pixels
|
|
74
|
+
* @param {Vector2} [frameSize] - Size of each frame, or the whole image if not passed
|
|
75
|
+
* @param {number} [padding] - How many pixels padding around each frame
|
|
76
|
+
* @return {TileInfo} Tile for the packed image, or undefined if the sheet is full */
|
|
77
|
+
tryAdd(imageSize, frameSize=imageSize, padding=textureSheetPadding)
|
|
78
|
+
{
|
|
79
|
+
ASSERT(isVector2(imageSize) && isVector2(frameSize), 'sizes must be vec2');
|
|
80
|
+
ASSERT(frameSize.x > 0 && frameSize.y > 0, 'frame size must be positive');
|
|
81
|
+
ASSERT(imageSize.x % frameSize.x === 0 && imageSize.y % frameSize.y === 0,
|
|
82
|
+
'image size must be a multiple of the frame size');
|
|
83
|
+
|
|
84
|
+
const cellWidth = frameSize.x + padding*2;
|
|
85
|
+
const cellHeight = frameSize.y + padding*2;
|
|
86
|
+
const maxColumns = this.size / cellWidth | 0;
|
|
87
|
+
ASSERT(maxColumns > 0, 'frame is too wide to fit on a texture sheet');
|
|
88
|
+
|
|
89
|
+
// keep the layout of the source image, but narrow it if a row is too wide
|
|
90
|
+
// frames wrap down to the next row, which TileInfo.frame handles via columns
|
|
91
|
+
const sourceColumns = imageSize.x / frameSize.x;
|
|
92
|
+
const frameCount = sourceColumns * (imageSize.y / frameSize.y);
|
|
93
|
+
const columns = min(sourceColumns, maxColumns);
|
|
94
|
+
const blockWidth = columns * cellWidth;
|
|
95
|
+
const blockHeight = ceil(frameCount / columns) * cellHeight;
|
|
96
|
+
|
|
97
|
+
// probe the placement using locals so a failed try leaves the sheet unchanged
|
|
98
|
+
let x = this.cursor.x, y = this.cursor.y, rowHeight = this.rowHeight;
|
|
99
|
+
if (x + blockWidth > this.size)
|
|
100
|
+
{
|
|
101
|
+
// start a new row if this one does not have enough space left
|
|
102
|
+
x = 0;
|
|
103
|
+
y += rowHeight;
|
|
104
|
+
rowHeight = 0;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// out of space, the caller needs to use a different sheet
|
|
108
|
+
if (y + blockHeight > this.size)
|
|
109
|
+
return undefined;
|
|
110
|
+
|
|
111
|
+
// commit the placement, tile pos points inside the padding to match how tile() works
|
|
112
|
+
this.cursor.x = x + blockWidth;
|
|
113
|
+
this.cursor.y = y;
|
|
114
|
+
this.rowHeight = max(rowHeight, blockHeight);
|
|
115
|
+
return new TileInfo(vec2(x + padding, y + padding), frameSize, this.textureInfo, padding, 0, columns);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** Draw an image into this sheet at a tile returned by tryAdd
|
|
119
|
+
* @param {HTMLImageElement} image - Source image to copy from
|
|
120
|
+
* @param {TileInfo} tileInfo - Where to put it, from tryAdd
|
|
121
|
+
* @param {boolean} [update] - Upload to webgl now, pass false when batching */
|
|
122
|
+
drawImage(image, tileInfo, update=true)
|
|
123
|
+
{
|
|
124
|
+
ASSERT(!!this.context, 'texture sheet has no canvas');
|
|
125
|
+
|
|
126
|
+
// copy frames in order, reading the source left to right, top to bottom
|
|
127
|
+
// the destination wraps at tileInfo.columns which may be narrower than the source
|
|
128
|
+
const frameSize = tileInfo.size;
|
|
129
|
+
const sourceColumns = image.width / frameSize.x;
|
|
130
|
+
const frameCount = sourceColumns * (image.height / frameSize.y);
|
|
131
|
+
const columns = tileInfo.columns || frameCount;
|
|
132
|
+
const cellWidth = frameSize.x + tileInfo.padding*2;
|
|
133
|
+
const cellHeight = frameSize.y + tileInfo.padding*2;
|
|
134
|
+
for (let i = frameCount; i--;)
|
|
135
|
+
{
|
|
136
|
+
const sourceX = (i % sourceColumns) * frameSize.x;
|
|
137
|
+
const sourceY = (i / sourceColumns | 0) * frameSize.y;
|
|
138
|
+
this.context.drawImage(image,
|
|
139
|
+
sourceX, sourceY, frameSize.x, frameSize.y,
|
|
140
|
+
tileInfo.pos.x + (i % columns) * cellWidth,
|
|
141
|
+
tileInfo.pos.y + (i / columns | 0) * cellHeight,
|
|
142
|
+
frameSize.x, frameSize.y);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// upload now unless the caller is batching more images
|
|
146
|
+
this.glDirty = true;
|
|
147
|
+
update && this.updateTexture();
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** Upload the canvas to webgl if it has changed since the last upload
|
|
151
|
+
* Only needed after batching, drawImage uploads automatically by default */
|
|
152
|
+
updateTexture()
|
|
153
|
+
{
|
|
154
|
+
if (!this.glDirty) return;
|
|
155
|
+
this.glDirty = false;
|
|
156
|
+
this.textureInfo.createWebGLTexture();
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
161
|
+
|
|
162
|
+
/** Load an image and pack it into a texture sheet
|
|
163
|
+
* - Returns a TileInfo immediately which is filled in when the image loads
|
|
164
|
+
* - Nothing is visible until it loads, use spritesReady to wait for it
|
|
165
|
+
* - Pass frameSize for animations, then step through them with TileInfo.frame
|
|
166
|
+
* - Grid images keep their layout and frames wrap down to the next row
|
|
167
|
+
* @param {string} src - Image source path
|
|
168
|
+
* @param {Vector2|number} [frameSize] - Size of each animation frame in pixels
|
|
169
|
+
* @param {number} [padding] - How many pixels padding around each frame
|
|
170
|
+
* @return {TileInfo}
|
|
171
|
+
* @example
|
|
172
|
+
* const playerTile = loadSprite('player.png'); // a single sprite
|
|
173
|
+
* const runTile = loadSprite('run.png', vec2(16)); // a 16x16 frame animation
|
|
174
|
+
* @memberof TextureSheets */
|
|
175
|
+
function loadSprite(src, frameSize, padding=textureSheetPadding)
|
|
176
|
+
{
|
|
177
|
+
ASSERT(isStringLike(src), 'image src must be a string');
|
|
178
|
+
ASSERT(!frameSize || isVector2(frameSize) || isNumber(frameSize), 'frameSize must be a vec2 or number');
|
|
179
|
+
ASSERT(isNumber(padding), 'padding must be a number');
|
|
180
|
+
|
|
181
|
+
if (isNumber(frameSize))
|
|
182
|
+
frameSize = vec2(frameSize);
|
|
183
|
+
|
|
184
|
+
// start with an empty tile that gets filled in when the image loads
|
|
185
|
+
const tileInfo = new TileInfo(vec2(), vec2(), undefined, padding, 0);
|
|
186
|
+
if (headlessMode) return tileInfo;
|
|
187
|
+
|
|
188
|
+
// point at a sheet right away so drawing before it loads picks up empty pixels
|
|
189
|
+
tileInfo.textureInfo = (textureSheets[0] || textureSheetCreate()).textureInfo;
|
|
190
|
+
|
|
191
|
+
// start decoding right away, images decode in parallel
|
|
192
|
+
const image = new Image;
|
|
193
|
+
const imagePromise = new Promise(resolve =>
|
|
194
|
+
{
|
|
195
|
+
image.onerror = image.onload = resolve;
|
|
196
|
+
image.crossOrigin = 'anonymous';
|
|
197
|
+
image.src = src;
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
// pack through a queue so sheets fill in call order, not decode order
|
|
201
|
+
++textureSheetPendingCount;
|
|
202
|
+
textureSheetQueue = textureSheetQueue.then(async ()=>
|
|
203
|
+
{
|
|
204
|
+
await imagePromise;
|
|
205
|
+
if (image.width)
|
|
206
|
+
{
|
|
207
|
+
// pack onto a sheet, then fill in the tile that was already handed out,
|
|
208
|
+
// copying every field so nothing is missed if TileInfo gains more of them
|
|
209
|
+
const imageSize = vec2(image.width, image.height);
|
|
210
|
+
const {sheet, tile} = textureSheetAdd(imageSize, frameSize, padding);
|
|
211
|
+
Object.assign(tileInfo, tile);
|
|
212
|
+
sheet.drawImage(image, tileInfo, false); // upload once per batch below
|
|
213
|
+
}
|
|
214
|
+
else
|
|
215
|
+
{
|
|
216
|
+
// leave the tile empty if the image failed to load
|
|
217
|
+
LOG('loadSprite failed to load image:', src);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// upload to webgl once per batch, when the last pending load finishes
|
|
221
|
+
if (!--textureSheetPendingCount)
|
|
222
|
+
textureSheets.forEach(s=> s.updateTexture());
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
return tileInfo;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/** Load a pre-packed texture atlas and repack it onto texture sheets
|
|
229
|
+
* - Supports TexturePacker json (hash and array) and Aseprite json
|
|
230
|
+
* - Returns an empty object which is filled with TileInfos when loaded
|
|
231
|
+
* - Frames are named by the json, animations are grouped automatically
|
|
232
|
+
* - Aseprite frame tags become animations, so do names like run_0, run_1
|
|
233
|
+
* - Trimmed frames are restored to their full source size when packed
|
|
234
|
+
* - Rotated frames are rotated back upright when packed
|
|
235
|
+
* @param {string} imageSrc - Atlas image path
|
|
236
|
+
* @param {string|Object} jsonSrc - Atlas json path, or already parsed json data
|
|
237
|
+
* @param {number} [padding] - How many pixels padding around each frame
|
|
238
|
+
* @return {Object} Object mapping frame and animation names to TileInfos
|
|
239
|
+
* @example
|
|
240
|
+
* const atlas = loadAtlas('sprites.png', 'sprites.json');
|
|
241
|
+
* await spritesReady();
|
|
242
|
+
* drawTile(pos, size, atlas.player); // a single frame
|
|
243
|
+
* drawTile(pos, size, atlas.run.frame(2)); // frame 2 of the run animation
|
|
244
|
+
* @memberof TextureSheets */
|
|
245
|
+
function loadAtlas(imageSrc, jsonSrc, padding=textureSheetPadding)
|
|
246
|
+
{
|
|
247
|
+
ASSERT(isStringLike(imageSrc), 'atlas image src must be a string');
|
|
248
|
+
ASSERT(isStringLike(jsonSrc) || typeof jsonSrc === 'object', 'atlas json must be a path or object');
|
|
249
|
+
ASSERT(isNumber(padding), 'padding must be a number');
|
|
250
|
+
|
|
251
|
+
const atlas = {};
|
|
252
|
+
if (headlessMode) return atlas;
|
|
253
|
+
|
|
254
|
+
// start fetching the json and decoding the image right away, in parallel
|
|
255
|
+
const jsonPromise = typeof jsonSrc === 'object' ? Promise.resolve(jsonSrc) :
|
|
256
|
+
fetch(jsonSrc).then(r=> r.ok && r.json()).catch(()=> undefined);
|
|
257
|
+
const image = new Image;
|
|
258
|
+
const imagePromise = new Promise(resolve =>
|
|
259
|
+
{
|
|
260
|
+
image.onerror = image.onload = resolve;
|
|
261
|
+
image.crossOrigin = 'anonymous';
|
|
262
|
+
image.src = imageSrc;
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
// pack through a queue so sheets fill in call order, not decode order
|
|
266
|
+
++textureSheetPendingCount;
|
|
267
|
+
textureSheetQueue = textureSheetQueue.then(async ()=>
|
|
268
|
+
{
|
|
269
|
+
const data = await jsonPromise;
|
|
270
|
+
await imagePromise;
|
|
271
|
+
if (image.width && data)
|
|
272
|
+
{
|
|
273
|
+
for (const group of parseAtlas(data))
|
|
274
|
+
{
|
|
275
|
+
// reserve a block of full size cells, one per frame
|
|
276
|
+
const sourceSize = group.frames[0].sourceSize;
|
|
277
|
+
const blockSize = vec2(sourceSize.x*group.frames.length, sourceSize.y);
|
|
278
|
+
const {sheet, tile} = textureSheetAdd(blockSize, sourceSize, padding);
|
|
279
|
+
|
|
280
|
+
// draw each frame untrimmed into its cell
|
|
281
|
+
const context = sheet.context;
|
|
282
|
+
const cellWidth = sourceSize.x + padding*2;
|
|
283
|
+
const cellHeight = sourceSize.y + padding*2;
|
|
284
|
+
group.frames.forEach((f, i)=>
|
|
285
|
+
{
|
|
286
|
+
const x = tile.pos.x + (i % tile.columns)*cellWidth + f.offset.x;
|
|
287
|
+
const y = tile.pos.y + (i / tile.columns |0)*cellHeight + f.offset.y;
|
|
288
|
+
if (f.rotated)
|
|
289
|
+
{
|
|
290
|
+
// stored rotated 90 degrees clockwise, draw it back upright
|
|
291
|
+
context.save();
|
|
292
|
+
context.translate(x, y);
|
|
293
|
+
context.rotate(-PI/2);
|
|
294
|
+
context.drawImage(image, f.pos.x, f.pos.y, f.size.y, f.size.x,
|
|
295
|
+
-f.size.y, 0, f.size.y, f.size.x);
|
|
296
|
+
context.restore();
|
|
297
|
+
}
|
|
298
|
+
else
|
|
299
|
+
context.drawImage(image, f.pos.x, f.pos.y, f.size.x, f.size.y,
|
|
300
|
+
x, y, f.size.x, f.size.y);
|
|
301
|
+
});
|
|
302
|
+
sheet.glDirty = true;
|
|
303
|
+
atlas[group.name] = tile;
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
else
|
|
307
|
+
{
|
|
308
|
+
// leave the atlas empty if either file failed to load
|
|
309
|
+
LOG('loadAtlas failed to load:', imageSrc, jsonSrc);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// upload to webgl once per batch, when the last pending load finishes
|
|
313
|
+
if (!--textureSheetPendingCount)
|
|
314
|
+
textureSheets.forEach(s=> s.updateTexture());
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
return atlas;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/** Parse atlas json into a list of named frame groups, used by loadAtlas
|
|
321
|
+
* - Accepts TexturePacker json (hash and array) and Aseprite json
|
|
322
|
+
* - Frames tagged in Aseprite or named like run_0, run_1 group into animations
|
|
323
|
+
* @param {Object} data - Parsed atlas json data
|
|
324
|
+
* @return {Array<Object>} List of {name, frames} groups in atlas order
|
|
325
|
+
* @memberof TextureSheets */
|
|
326
|
+
function parseAtlas(data)
|
|
327
|
+
{
|
|
328
|
+
ASSERT(!!data?.frames, 'unrecognized atlas format, expected TexturePacker or Aseprite json');
|
|
329
|
+
|
|
330
|
+
// normalize both hash and array frame layouts into a single list
|
|
331
|
+
const frames = (isArray(data.frames) ?
|
|
332
|
+
data.frames.map(f=> [f.filename, f]) : Object.entries(data.frames))
|
|
333
|
+
.map(([name, f])=> ({
|
|
334
|
+
name: name.replace(/\.[^.\\/]+$/, ''), // strip file extension
|
|
335
|
+
pos: vec2(f.frame.x, f.frame.y),
|
|
336
|
+
size: vec2(f.frame.w, f.frame.h),
|
|
337
|
+
offset: vec2(f.spriteSourceSize?.x ?? 0, f.spriteSourceSize?.y ?? 0),
|
|
338
|
+
sourceSize: vec2(f.sourceSize?.w ?? f.frame.w, f.sourceSize?.h ?? f.frame.h),
|
|
339
|
+
rotated: !!f.rotated,
|
|
340
|
+
}));
|
|
341
|
+
|
|
342
|
+
const groups = [];
|
|
343
|
+
const tags = data.meta?.frameTags;
|
|
344
|
+
if (tags?.length)
|
|
345
|
+
{
|
|
346
|
+
// aseprite tags are authoritative, untagged frames stay individual
|
|
347
|
+
const tagged = new Set;
|
|
348
|
+
for (const tag of tags)
|
|
349
|
+
{
|
|
350
|
+
groups.push({name: tag.name, frames: frames.slice(tag.from, tag.to + 1)});
|
|
351
|
+
for (let i = tag.from; i <= tag.to; ++i)
|
|
352
|
+
tagged.add(i);
|
|
353
|
+
}
|
|
354
|
+
frames.forEach((f, i)=> tagged.has(i) || groups.push({name: f.name, frames: [f]}));
|
|
355
|
+
return groups;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// group frames that share a name stem with contiguous trailing numbers
|
|
359
|
+
// run_0.png and run_1.png become a 2 frame animation named run
|
|
360
|
+
const stems = new Map;
|
|
361
|
+
for (const f of frames)
|
|
362
|
+
{
|
|
363
|
+
let match = f.name.match(/^(.+?)([-_ ])?(\d+)$/);
|
|
364
|
+
if (match && !match[2] && /\d$/.test(match[1]))
|
|
365
|
+
match = undefined; // all digit tails like 10 are a name, not frame 0 of 1
|
|
366
|
+
const stem = match ? match[1] : f.name;
|
|
367
|
+
f.groupIndex = match ? Number(match[3]) : undefined;
|
|
368
|
+
stems.has(stem) || stems.set(stem, []);
|
|
369
|
+
stems.get(stem).push(f);
|
|
370
|
+
}
|
|
371
|
+
for (const [stem, list] of stems)
|
|
372
|
+
{
|
|
373
|
+
// only group 2 or more frames with contiguous indices and matching sizes
|
|
374
|
+
list.sort((a, b)=> a.groupIndex - b.groupIndex);
|
|
375
|
+
const grouped = list.length > 1 &&
|
|
376
|
+
list.every((f, i)=> f.groupIndex === list[0].groupIndex + i) &&
|
|
377
|
+
list.every(f=> f.sourceSize.x === list[0].sourceSize.x &&
|
|
378
|
+
f.sourceSize.y === list[0].sourceSize.y);
|
|
379
|
+
if (grouped)
|
|
380
|
+
groups.push({name: stem, frames: list});
|
|
381
|
+
else
|
|
382
|
+
list.forEach(f=> groups.push({name: f.name, frames: [f]}));
|
|
383
|
+
}
|
|
384
|
+
return groups;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
/** Wait for everything started by loadSprite and loadAtlas to finish packing
|
|
388
|
+
* @return {Promise}
|
|
389
|
+
* @example
|
|
390
|
+
* async function gameInit()
|
|
391
|
+
* {
|
|
392
|
+
* playerTile = loadSprite('player.png');
|
|
393
|
+
* runTile = loadSprite('run.png', vec2(16));
|
|
394
|
+
* await spritesReady();
|
|
395
|
+
* }
|
|
396
|
+
* @memberof TextureSheets */
|
|
397
|
+
async function spritesReady()
|
|
398
|
+
{
|
|
399
|
+
// keep waiting until the queue drains, more sprites may load while waiting
|
|
400
|
+
while (textureSheetPendingCount)
|
|
401
|
+
await textureSheetQueue;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
// create a new texture sheet and add it to the list
|
|
405
|
+
function textureSheetCreate()
|
|
406
|
+
{
|
|
407
|
+
const sheet = new TextureSheet;
|
|
408
|
+
textureSheets.push(sheet);
|
|
409
|
+
return sheet;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
// use the first sheet with enough space, or make a new one
|
|
413
|
+
function textureSheetAdd(imageSize, frameSize, padding)
|
|
414
|
+
{
|
|
415
|
+
let sheet, tile;
|
|
416
|
+
for (sheet of textureSheets)
|
|
417
|
+
if (tile = sheet.tryAdd(imageSize, frameSize, padding))
|
|
418
|
+
break;
|
|
419
|
+
if (!tile)
|
|
420
|
+
{
|
|
421
|
+
sheet = textureSheetCreate();
|
|
422
|
+
tile = sheet.tryAdd(imageSize, frameSize, padding);
|
|
423
|
+
ASSERT(!!tile, 'image is too large to fit on a texture sheet');
|
|
424
|
+
}
|
|
425
|
+
return {sheet, tile};
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
429
|
+
// Texture sheet setting setters
|
|
430
|
+
|
|
431
|
+
/** Set width and height in pixels of texture sheets created by loadSprite
|
|
432
|
+
* @param {number} size
|
|
433
|
+
* @memberof Settings */
|
|
434
|
+
function setTextureSheetSize(size) { textureSheetSize = size; }
|
|
435
|
+
|
|
436
|
+
/** Set default padding pixels around each frame packed by loadSprite
|
|
437
|
+
* @param {number} padding
|
|
438
|
+
* @memberof Settings */
|
|
439
|
+
function setTextureSheetPadding(padding) { textureSheetPadding = padding; }
|