littlejsengine 1.18.21 → 1.18.23

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -35,7 +35,7 @@ const engineName = 'LittleJS';
35
35
  * @type {string}
36
36
  * @default
37
37
  * @memberof Engine */
38
- const engineVersion = '1.18.21';
38
+ const engineVersion = '1.18.23';
39
39
 
40
40
  /** Frames per second to update
41
41
  * @type {number}
@@ -4167,8 +4167,9 @@ class TileInfo
4167
4167
  * @param {TextureInfo} [textureInfo] - Texture info to use
4168
4168
  * @param {number} [padding] - How many pixels padding around all sides of each tile (increases grid size, does not affect tile size)
4169
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
4170
4171
  */
4171
- 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)
4172
4173
  {
4173
4174
  /** @property {Vector2} - Top left corner of tile in pixels */
4174
4175
  this.pos = pos.copy();
@@ -4180,6 +4181,8 @@ class TileInfo
4180
4181
  this.textureInfo = textureInfo;
4181
4182
  /** @property {number} - Shrinks tile by this many pixels to prevent neighbors bleeding */
4182
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;
4183
4186
  }
4184
4187
 
4185
4188
  /** Returns a copy of this tile offset by a vector
@@ -4187,9 +4190,10 @@ class TileInfo
4187
4190
  * @return {TileInfo}
4188
4191
  */
4189
4192
  offset(offset)
4190
- { 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); }
4191
4194
 
4192
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
4193
4197
  * @param {number} frame - Offset to apply in animation frames
4194
4198
  * @return {TileInfo}
4195
4199
  */
@@ -4197,9 +4201,23 @@ class TileInfo
4197
4201
  {
4198
4202
  ASSERT(typeof frame === 'number');
4199
4203
  const w = this.size.x + this.padding*2;
4200
- const x = frame*w;
4201
- ASSERT(x + this.size.x <= this.textureInfo.size.x, 'frame extends beyond texture width!');
4202
- return this.offset(new Vector2(x));
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;
4203
4221
  }
4204
4222
 
4205
4223
  /**
@@ -4208,7 +4226,7 @@ class TileInfo
4208
4226
  * @return {TileInfo}
4209
4227
  */
4210
4228
  index(index)
4211
- { return tile(index, this.size, this.textureInfo, this.padding, this.bleed); }
4229
+ { return tile(index, this.size, this.textureInfo, this.padding, this.bleed).setColumns(this.columns); }
4212
4230
 
4213
4231
  /**
4214
4232
  * Set this tile to use a full image in a texture info
@@ -4220,7 +4238,7 @@ class TileInfo
4220
4238
  this.textureInfo = textureInfo;
4221
4239
  this.pos = new Vector2;
4222
4240
  this.size = textureInfo.size.copy();
4223
- this.bleed = this.padding = 0;
4241
+ this.bleed = this.padding = this.columns = 0;
4224
4242
  return this;
4225
4243
  }
4226
4244
  }
@@ -14907,6 +14925,455 @@ function getCrescentPoints(pos, size=1, percent=0, angle=0, invert=false, sides=
14907
14925
  }
14908
14926
  return points;
14909
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
+ * @param {number} [sourcePadding] - How many pixels padding around each frame in the source image
15002
+ * @return {TileInfo} Tile for the packed image, or undefined if the sheet is full */
15003
+ tryAdd(imageSize, frameSize=imageSize, padding=textureSheetPadding, sourcePadding=0)
15004
+ {
15005
+ ASSERT(isVector2(imageSize) && isVector2(frameSize), 'sizes must be vec2');
15006
+ ASSERT(frameSize.x > 0 && frameSize.y > 0, 'frame size must be positive');
15007
+ ASSERT(isNumber(sourcePadding) && sourcePadding >= 0, 'sourcePadding must be a number >= 0');
15008
+
15009
+ // the source may have its own padding baked in around each frame
15010
+ const sourceCellWidth = frameSize.x + sourcePadding*2;
15011
+ const sourceCellHeight = frameSize.y + sourcePadding*2;
15012
+ ASSERT(imageSize.x % sourceCellWidth === 0 && imageSize.y % sourceCellHeight === 0,
15013
+ 'image size must be a multiple of the padded frame size');
15014
+
15015
+ const cellWidth = frameSize.x + padding*2;
15016
+ const cellHeight = frameSize.y + padding*2;
15017
+ const maxColumns = this.size / cellWidth | 0;
15018
+ ASSERT(maxColumns > 0, 'frame is too wide to fit on a texture sheet');
15019
+
15020
+ // keep the layout of the source image, but narrow it if a row is too wide
15021
+ // frames wrap down to the next row, which TileInfo.frame handles via columns
15022
+ const sourceColumns = imageSize.x / sourceCellWidth;
15023
+ const frameCount = sourceColumns * (imageSize.y / sourceCellHeight);
15024
+ const columns = min(sourceColumns, maxColumns);
15025
+ const blockWidth = columns * cellWidth;
15026
+ const blockHeight = ceil(frameCount / columns) * cellHeight;
15027
+
15028
+ // probe the placement using locals so a failed try leaves the sheet unchanged
15029
+ let x = this.cursor.x, y = this.cursor.y, rowHeight = this.rowHeight;
15030
+ if (x + blockWidth > this.size)
15031
+ {
15032
+ // start a new row if this one does not have enough space left
15033
+ x = 0;
15034
+ y += rowHeight;
15035
+ rowHeight = 0;
15036
+ }
15037
+
15038
+ // out of space, the caller needs to use a different sheet
15039
+ if (y + blockHeight > this.size)
15040
+ return undefined;
15041
+
15042
+ // commit the placement, tile pos points inside the padding to match how tile() works
15043
+ this.cursor.x = x + blockWidth;
15044
+ this.cursor.y = y;
15045
+ this.rowHeight = max(rowHeight, blockHeight);
15046
+ return new TileInfo(vec2(x + padding, y + padding), frameSize, this.textureInfo, padding, 0, columns);
15047
+ }
15048
+
15049
+ /** Draw an image into this sheet at a tile returned by tryAdd
15050
+ * @param {HTMLImageElement} image - Source image to copy from
15051
+ * @param {TileInfo} tileInfo - Where to put it, from tryAdd
15052
+ * @param {boolean} [update] - Upload to webgl now, pass false when batching
15053
+ * @param {number} [sourcePadding] - How many pixels padding around each frame in the source image */
15054
+ drawImage(image, tileInfo, update=true, sourcePadding=0)
15055
+ {
15056
+ ASSERT(!!this.context, 'texture sheet has no canvas');
15057
+
15058
+ // copy frames in order, reading the source left to right, top to bottom
15059
+ // the destination wraps at tileInfo.columns which may be narrower than the source
15060
+ const frameSize = tileInfo.size;
15061
+ const sourceCellWidth = frameSize.x + sourcePadding*2;
15062
+ const sourceCellHeight = frameSize.y + sourcePadding*2;
15063
+ const sourceColumns = image.width / sourceCellWidth;
15064
+ const frameCount = sourceColumns * (image.height / sourceCellHeight);
15065
+ const columns = tileInfo.columns || frameCount;
15066
+ const cellWidth = frameSize.x + tileInfo.padding*2;
15067
+ const cellHeight = frameSize.y + tileInfo.padding*2;
15068
+ for (let i = frameCount; i--;)
15069
+ {
15070
+ const sourceX = (i % sourceColumns) * sourceCellWidth + sourcePadding;
15071
+ const sourceY = (i / sourceColumns | 0) * sourceCellHeight + sourcePadding;
15072
+ this.context.drawImage(image,
15073
+ sourceX, sourceY, frameSize.x, frameSize.y,
15074
+ tileInfo.pos.x + (i % columns) * cellWidth,
15075
+ tileInfo.pos.y + (i / columns | 0) * cellHeight,
15076
+ frameSize.x, frameSize.y);
15077
+ }
15078
+
15079
+ // upload now unless the caller is batching more images
15080
+ this.glDirty = true;
15081
+ update && this.updateTexture();
15082
+ }
15083
+
15084
+ /** Upload the canvas to webgl if it has changed since the last upload
15085
+ * Only needed after batching, drawImage uploads automatically by default */
15086
+ updateTexture()
15087
+ {
15088
+ if (!this.glDirty) return;
15089
+ this.glDirty = false;
15090
+ this.textureInfo.createWebGLTexture();
15091
+ }
15092
+ }
15093
+
15094
+ ///////////////////////////////////////////////////////////////////////////////
15095
+
15096
+ /** Load an image and pack it into a texture sheet
15097
+ * - Returns a TileInfo immediately which is filled in when the image loads
15098
+ * - Nothing is visible until it loads, use spritesReady to wait for it
15099
+ * - Pass frameSize for animations, then step through them with TileInfo.frame
15100
+ * - Grid images keep their layout and frames wrap down to the next row
15101
+ * - Pass sourcePadding if the source image has padding baked in around frames
15102
+ * @param {string} src - Image source path
15103
+ * @param {Vector2|number} [frameSize] - Size of each animation frame in pixels
15104
+ * @param {number} [padding] - How many pixels padding around each frame
15105
+ * @param {number} [sourcePadding] - How many pixels padding around each frame in the source image
15106
+ * @return {TileInfo}
15107
+ * @example
15108
+ * const playerTile = loadSprite('player.png'); // a single sprite
15109
+ * const runTile = loadSprite('run.png', vec2(16)); // a 16x16 frame animation
15110
+ * @memberof TextureSheets */
15111
+ function loadSprite(src, frameSize, padding=textureSheetPadding, sourcePadding=0)
15112
+ {
15113
+ ASSERT(isStringLike(src), 'image src must be a string');
15114
+ ASSERT(!frameSize || isVector2(frameSize) || isNumber(frameSize), 'frameSize must be a vec2 or number');
15115
+ ASSERT(isNumber(padding), 'padding must be a number');
15116
+
15117
+ if (isNumber(frameSize))
15118
+ frameSize = vec2(frameSize);
15119
+
15120
+ // start with an empty tile that gets filled in when the image loads
15121
+ const tileInfo = new TileInfo(vec2(), vec2(), undefined, padding, 0);
15122
+ if (headlessMode) return tileInfo;
15123
+
15124
+ // point at a sheet right away so drawing before it loads picks up empty pixels
15125
+ tileInfo.textureInfo = (textureSheets[0] || textureSheetCreate()).textureInfo;
15126
+
15127
+ // start decoding right away, images decode in parallel
15128
+ const image = new Image;
15129
+ const imagePromise = new Promise(resolve =>
15130
+ {
15131
+ image.onerror = image.onload = resolve;
15132
+ image.crossOrigin = 'anonymous';
15133
+ image.src = src;
15134
+ });
15135
+
15136
+ // pack through a queue so sheets fill in call order, not decode order
15137
+ ++textureSheetPendingCount;
15138
+ textureSheetQueue = textureSheetQueue.then(async ()=>
15139
+ {
15140
+ await imagePromise;
15141
+ if (image.width)
15142
+ {
15143
+ // pack onto a sheet, then fill in the tile that was already handed out,
15144
+ // copying every field so nothing is missed if TileInfo gains more of them
15145
+ const imageSize = vec2(image.width, image.height);
15146
+ const {sheet, tile} = textureSheetAdd(imageSize, frameSize, padding, sourcePadding);
15147
+ Object.assign(tileInfo, tile);
15148
+ sheet.drawImage(image, tileInfo, false, sourcePadding); // upload once per batch below
15149
+ }
15150
+ else
15151
+ {
15152
+ // leave the tile empty if the image failed to load
15153
+ LOG('loadSprite failed to load image:', src);
15154
+ }
15155
+
15156
+ // upload to webgl once per batch, when the last pending load finishes
15157
+ if (!--textureSheetPendingCount)
15158
+ textureSheets.forEach(s=> s.updateTexture());
15159
+ });
15160
+
15161
+ return tileInfo;
15162
+ }
15163
+
15164
+ /** Load a pre-packed texture atlas and repack it onto texture sheets
15165
+ * - Supports TexturePacker json (hash and array) and Aseprite json
15166
+ * - Returns an empty object which is filled with TileInfos when loaded
15167
+ * - Frames are named by the json, animations are grouped automatically
15168
+ * - Aseprite frame tags become animations, so do names like run_0, run_1
15169
+ * - Trimmed frames are restored to their full source size when packed
15170
+ * - Rotated frames are rotated back upright when packed
15171
+ * @param {string} imageSrc - Atlas image path
15172
+ * @param {string|Object} jsonSrc - Atlas json path, or already parsed json data
15173
+ * @param {number} [padding] - How many pixels padding around each frame
15174
+ * @return {Object} Object mapping frame and animation names to TileInfos
15175
+ * @example
15176
+ * const atlas = loadAtlas('sprites.png', 'sprites.json');
15177
+ * await spritesReady();
15178
+ * drawTile(pos, size, atlas.player); // a single frame
15179
+ * drawTile(pos, size, atlas.run.frame(2)); // frame 2 of the run animation
15180
+ * @memberof TextureSheets */
15181
+ function loadAtlas(imageSrc, jsonSrc, padding=textureSheetPadding)
15182
+ {
15183
+ ASSERT(isStringLike(imageSrc), 'atlas image src must be a string');
15184
+ ASSERT(isStringLike(jsonSrc) || typeof jsonSrc === 'object', 'atlas json must be a path or object');
15185
+ ASSERT(isNumber(padding), 'padding must be a number');
15186
+
15187
+ const atlas = {};
15188
+ if (headlessMode) return atlas;
15189
+
15190
+ // start fetching the json and decoding the image right away, in parallel
15191
+ const jsonPromise = typeof jsonSrc === 'object' ? Promise.resolve(jsonSrc) :
15192
+ fetch(jsonSrc).then(r=> r.ok && r.json()).catch(()=> undefined);
15193
+ const image = new Image;
15194
+ const imagePromise = new Promise(resolve =>
15195
+ {
15196
+ image.onerror = image.onload = resolve;
15197
+ image.crossOrigin = 'anonymous';
15198
+ image.src = imageSrc;
15199
+ });
15200
+
15201
+ // pack through a queue so sheets fill in call order, not decode order
15202
+ ++textureSheetPendingCount;
15203
+ textureSheetQueue = textureSheetQueue.then(async ()=>
15204
+ {
15205
+ const data = await jsonPromise;
15206
+ await imagePromise;
15207
+ if (image.width && data)
15208
+ {
15209
+ for (const group of parseAtlas(data))
15210
+ {
15211
+ // reserve a block of full size cells, one per frame
15212
+ const sourceSize = group.frames[0].sourceSize;
15213
+ const blockSize = vec2(sourceSize.x*group.frames.length, sourceSize.y);
15214
+ const {sheet, tile} = textureSheetAdd(blockSize, sourceSize, padding);
15215
+
15216
+ // draw each frame untrimmed into its cell
15217
+ const context = sheet.context;
15218
+ const cellWidth = sourceSize.x + padding*2;
15219
+ const cellHeight = sourceSize.y + padding*2;
15220
+ group.frames.forEach((f, i)=>
15221
+ {
15222
+ const x = tile.pos.x + (i % tile.columns)*cellWidth + f.offset.x;
15223
+ const y = tile.pos.y + (i / tile.columns |0)*cellHeight + f.offset.y;
15224
+ if (f.rotated)
15225
+ {
15226
+ // stored rotated 90 degrees clockwise, draw it back upright
15227
+ context.save();
15228
+ context.translate(x, y);
15229
+ context.rotate(-PI/2);
15230
+ context.drawImage(image, f.pos.x, f.pos.y, f.size.y, f.size.x,
15231
+ -f.size.y, 0, f.size.y, f.size.x);
15232
+ context.restore();
15233
+ }
15234
+ else
15235
+ context.drawImage(image, f.pos.x, f.pos.y, f.size.x, f.size.y,
15236
+ x, y, f.size.x, f.size.y);
15237
+ });
15238
+ sheet.glDirty = true;
15239
+ atlas[group.name] = tile;
15240
+ }
15241
+ }
15242
+ else
15243
+ {
15244
+ // leave the atlas empty if either file failed to load
15245
+ LOG('loadAtlas failed to load:', imageSrc, jsonSrc);
15246
+ }
15247
+
15248
+ // upload to webgl once per batch, when the last pending load finishes
15249
+ if (!--textureSheetPendingCount)
15250
+ textureSheets.forEach(s=> s.updateTexture());
15251
+ });
15252
+
15253
+ return atlas;
15254
+ }
15255
+
15256
+ /** Parse atlas json into a list of named frame groups, used by loadAtlas
15257
+ * - Accepts TexturePacker json (hash and array) and Aseprite json
15258
+ * - Frames tagged in Aseprite or named like run_0, run_1 group into animations
15259
+ * @param {Object} data - Parsed atlas json data
15260
+ * @return {Array<Object>} List of {name, frames} groups in atlas order
15261
+ * @memberof TextureSheets */
15262
+ function parseAtlas(data)
15263
+ {
15264
+ ASSERT(!!data?.frames, 'unrecognized atlas format, expected TexturePacker or Aseprite json');
15265
+
15266
+ // normalize both hash and array frame layouts into a single list
15267
+ const frames = (isArray(data.frames) ?
15268
+ data.frames.map(f=> [f.filename, f]) : Object.entries(data.frames))
15269
+ .map(([name, f])=> ({
15270
+ name: name.replace(/\.[^.\\/]+$/, ''), // strip file extension
15271
+ pos: vec2(f.frame.x, f.frame.y),
15272
+ size: vec2(f.frame.w, f.frame.h),
15273
+ offset: vec2(f.spriteSourceSize?.x ?? 0, f.spriteSourceSize?.y ?? 0),
15274
+ sourceSize: vec2(f.sourceSize?.w ?? f.frame.w, f.sourceSize?.h ?? f.frame.h),
15275
+ rotated: !!f.rotated,
15276
+ }));
15277
+
15278
+ const groups = [];
15279
+ const tags = data.meta?.frameTags;
15280
+ if (tags?.length)
15281
+ {
15282
+ // aseprite tags are authoritative, untagged frames stay individual
15283
+ const tagged = new Set;
15284
+ for (const tag of tags)
15285
+ {
15286
+ groups.push({name: tag.name, frames: frames.slice(tag.from, tag.to + 1)});
15287
+ for (let i = tag.from; i <= tag.to; ++i)
15288
+ tagged.add(i);
15289
+ }
15290
+ frames.forEach((f, i)=> tagged.has(i) || groups.push({name: f.name, frames: [f]}));
15291
+ return groups;
15292
+ }
15293
+
15294
+ // group frames that share a name stem with contiguous trailing numbers
15295
+ // run_0.png and run_1.png become a 2 frame animation named run
15296
+ const stems = new Map;
15297
+ for (const f of frames)
15298
+ {
15299
+ let match = f.name.match(/^(.+?)([-_ ])?(\d+)$/);
15300
+ if (match && !match[2] && /\d$/.test(match[1]))
15301
+ match = undefined; // all digit tails like 10 are a name, not frame 0 of 1
15302
+ const stem = match ? match[1] : f.name;
15303
+ f.groupIndex = match ? Number(match[3]) : undefined;
15304
+ stems.has(stem) || stems.set(stem, []);
15305
+ stems.get(stem).push(f);
15306
+ }
15307
+ for (const [stem, list] of stems)
15308
+ {
15309
+ // only group 2 or more frames with contiguous indices and matching sizes
15310
+ list.sort((a, b)=> a.groupIndex - b.groupIndex);
15311
+ const grouped = list.length > 1 &&
15312
+ list.every((f, i)=> f.groupIndex === list[0].groupIndex + i) &&
15313
+ list.every(f=> f.sourceSize.x === list[0].sourceSize.x &&
15314
+ f.sourceSize.y === list[0].sourceSize.y);
15315
+ if (grouped)
15316
+ groups.push({name: stem, frames: list});
15317
+ else
15318
+ list.forEach(f=> groups.push({name: f.name, frames: [f]}));
15319
+ }
15320
+ return groups;
15321
+ }
15322
+
15323
+ /** Wait for everything started by loadSprite and loadAtlas to finish packing
15324
+ * @return {Promise}
15325
+ * @example
15326
+ * async function gameInit()
15327
+ * {
15328
+ * playerTile = loadSprite('player.png');
15329
+ * runTile = loadSprite('run.png', vec2(16));
15330
+ * await spritesReady();
15331
+ * }
15332
+ * @memberof TextureSheets */
15333
+ async function spritesReady()
15334
+ {
15335
+ // keep waiting until the queue drains, more sprites may load while waiting
15336
+ while (textureSheetPendingCount)
15337
+ await textureSheetQueue;
15338
+ }
15339
+
15340
+ // create a new texture sheet and add it to the list
15341
+ function textureSheetCreate()
15342
+ {
15343
+ const sheet = new TextureSheet;
15344
+ textureSheets.push(sheet);
15345
+ return sheet;
15346
+ }
15347
+
15348
+ // use the first sheet with enough space, or make a new one
15349
+ function textureSheetAdd(imageSize, frameSize, padding, sourcePadding)
15350
+ {
15351
+ let sheet, tile;
15352
+ for (sheet of textureSheets)
15353
+ if (tile = sheet.tryAdd(imageSize, frameSize, padding, sourcePadding))
15354
+ break;
15355
+ if (!tile)
15356
+ {
15357
+ sheet = textureSheetCreate();
15358
+ tile = sheet.tryAdd(imageSize, frameSize, padding, sourcePadding);
15359
+ ASSERT(!!tile, 'image is too large to fit on a texture sheet');
15360
+ }
15361
+ return {sheet, tile};
15362
+ }
15363
+
15364
+ ///////////////////////////////////////////////////////////////////////////////
15365
+ // Texture sheet setting setters
15366
+
15367
+ /** Set width and height in pixels of texture sheets created by loadSprite
15368
+ * @param {number} size
15369
+ * @memberof Settings */
15370
+ function setTextureSheetSize(size) { textureSheetSize = size; }
15371
+
15372
+ /** Set default padding pixels around each frame packed by loadSprite
15373
+ * @param {number} padding
15374
+ * @memberof Settings */
15375
+ function setTextureSheetPadding(padding) { textureSheetPadding = padding; }
15376
+
14910
15377
  /**
14911
15378
  * LittleJS Tween System Plugin
14912
15379
  * - Lightweight tweens for numbers, Vector2, Color, or any .lerp-able type
@@ -16865,4 +17332,17 @@ export
16865
17332
  threeJS,
16866
17333
  ThreeJSPlugin,
16867
17334
  ThreeJSObject,
16868
- }
17335
+
17336
+ // Texture Sheets
17337
+ textureSheetSize,
17338
+ textureSheetPadding,
17339
+ setTextureSheetSize,
17340
+ setTextureSheetPadding,
17341
+ textureSheets,
17342
+ TextureSheet,
17343
+ loadSprite,
17344
+ loadAtlas,
17345
+ parseAtlas,
17346
+ spritesReady,
17347
+ }
17348
+