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}
@@ -3476,8 +3476,9 @@ class TileInfo
3476
3476
  * @param {TextureInfo} [textureInfo] - Texture info to use
3477
3477
  * @param {number} [padding] - How many pixels padding around all sides of each tile (increases grid size, does not affect tile size)
3478
3478
  * @param {number} [bleed] - How many pixels smaller to shrink UVS of tiles (does not affect grid size, only UVs)
3479
+ * @param {number} [columns] - How many frames per row for frame(), 0 to keep frames on a single row
3479
3480
  */
3480
- constructor(pos=vec2(), size=tileDefaultSize, textureInfo=textureInfos[0], padding=tileDefaultPadding, bleed=tileDefaultBleed)
3481
+ constructor(pos=vec2(), size=tileDefaultSize, textureInfo=textureInfos[0], padding=tileDefaultPadding, bleed=tileDefaultBleed, columns=0)
3481
3482
  {
3482
3483
  /** @property {Vector2} - Top left corner of tile in pixels */
3483
3484
  this.pos = pos.copy();
@@ -3489,6 +3490,8 @@ class TileInfo
3489
3490
  this.textureInfo = textureInfo;
3490
3491
  /** @property {number} - Shrinks tile by this many pixels to prevent neighbors bleeding */
3491
3492
  this.bleed = bleed;
3493
+ /** @property {number} - How many frames per row for frame(), 0 to keep frames on a single row */
3494
+ this.columns = columns;
3492
3495
  }
3493
3496
 
3494
3497
  /** Returns a copy of this tile offset by a vector
@@ -3496,9 +3499,10 @@ class TileInfo
3496
3499
  * @return {TileInfo}
3497
3500
  */
3498
3501
  offset(offset)
3499
- { return new TileInfo(this.pos.add(offset), this.size, this.textureInfo, this.padding, this.bleed); }
3502
+ { return new TileInfo(this.pos.add(offset), this.size, this.textureInfo, this.padding, this.bleed, this.columns); }
3500
3503
 
3501
3504
  /** Returns a copy of this tile offset by a number of animation frames
3505
+ * Frames wrap down to the next row if columns is set
3502
3506
  * @param {number} frame - Offset to apply in animation frames
3503
3507
  * @return {TileInfo}
3504
3508
  */
@@ -3506,9 +3510,23 @@ class TileInfo
3506
3510
  {
3507
3511
  ASSERT(typeof frame === 'number');
3508
3512
  const w = this.size.x + this.padding*2;
3509
- const x = frame*w;
3510
- ASSERT(x + this.size.x <= this.textureInfo.size.x, 'frame extends beyond texture width!');
3511
- return this.offset(new Vector2(x));
3513
+ const h = this.size.y + this.padding*2;
3514
+ const x = (this.columns ? frame % this.columns : frame) * w;
3515
+ const y = (this.columns ? frame / this.columns | 0 : 0) * h;
3516
+ ASSERT(this.pos.x + x + this.size.x <= this.textureInfo.size.x, 'frame extends beyond texture width!');
3517
+ ASSERT(this.pos.y + y + this.size.y <= this.textureInfo.size.y, 'frame extends beyond texture height!');
3518
+ return this.offset(new Vector2(x, y));
3519
+ }
3520
+
3521
+ /** Set how many frames per row this tile uses, so frame() can wrap
3522
+ * @param {number} [columns] - Frames per row, 0 to keep frames on a single row
3523
+ * @return {TileInfo}
3524
+ */
3525
+ setColumns(columns=0)
3526
+ {
3527
+ ASSERT(isNumber(columns) && columns >= 0, 'columns must be a number >= 0');
3528
+ this.columns = columns;
3529
+ return this;
3512
3530
  }
3513
3531
 
3514
3532
  /**
@@ -3517,7 +3535,7 @@ class TileInfo
3517
3535
  * @return {TileInfo}
3518
3536
  */
3519
3537
  index(index)
3520
- { return tile(index, this.size, this.textureInfo, this.padding, this.bleed); }
3538
+ { return tile(index, this.size, this.textureInfo, this.padding, this.bleed).setColumns(this.columns); }
3521
3539
 
3522
3540
  /**
3523
3541
  * Set this tile to use a full image in a texture info
@@ -3529,7 +3547,7 @@ class TileInfo
3529
3547
  this.textureInfo = textureInfo;
3530
3548
  this.pos = new Vector2;
3531
3549
  this.size = textureInfo.size.copy();
3532
- this.bleed = this.padding = 0;
3550
+ this.bleed = this.padding = this.columns = 0;
3533
3551
  return this;
3534
3552
  }
3535
3553
  }
@@ -14216,6 +14234,455 @@ function getCrescentPoints(pos, size=1, percent=0, angle=0, invert=false, sides=
14216
14234
  }
14217
14235
  return points;
14218
14236
  }
14237
+ /**
14238
+ * LittleJS Texture Sheet Plugin
14239
+ * - Packs images into texture sheets as they are loaded
14240
+ * - Sprites are placed automatically, callers get a TileInfo
14241
+ * - Sheets are created and filled as needed
14242
+ * - Sheets fill in call order, images decode in parallel
14243
+ * - Animation frames keep layout and wrap across rows as needed
14244
+ * - WebGL textures upload once per batch of loads
14245
+ * - loadAtlas imports pre-packed atlases (TexturePacker and Aseprite json)
14246
+ * @namespace TextureSheets
14247
+ */
14248
+
14249
+ /** Width and height in pixels of texture sheets created by loadSprite
14250
+ * @type {number}
14251
+ * @default
14252
+ * @memberof Settings */
14253
+ let textureSheetSize = 2048;
14254
+
14255
+ /** Default padding pixels around each frame packed by loadSprite
14256
+ * @type {number}
14257
+ * @default
14258
+ * @memberof Settings */
14259
+ let textureSheetPadding = 1;
14260
+
14261
+ /** Array of texture sheets created by loadSprite
14262
+ * @type {Array<TextureSheet>}
14263
+ * @memberof TextureSheets */
14264
+ let textureSheets = [];
14265
+
14266
+ // pending loads pack through a queue so sheets fill in call order
14267
+ let textureSheetQueue = Promise.resolve();
14268
+ let textureSheetPendingCount = 0;
14269
+
14270
+ /**
14271
+ * Texture Sheet - A texture that images are packed into as they load
14272
+ * Uses shelf packing, images are placed left to right then wrap to a new row
14273
+ * @memberof TextureSheets
14274
+ */
14275
+ class TextureSheet
14276
+ {
14277
+ /** Create a texture sheet, called automatically by loadSprite
14278
+ * @param {number} [size] - Width and height of the sheet in pixels */
14279
+ constructor(size=textureSheetSize)
14280
+ {
14281
+ ASSERT(size > 0, 'texture sheet size must be positive');
14282
+
14283
+ /** @property {number} - Width and height of the sheet in pixels */
14284
+ this.size = size;
14285
+ /** @property {OffscreenCanvas} - Canvas holding the packed images */
14286
+ this.canvas = headlessMode ? undefined : new OffscreenCanvas(size, size);
14287
+ /** @property {OffscreenCanvasRenderingContext2D} - 2d context for the canvas */
14288
+ this.context = this.canvas?.getContext('2d');
14289
+ /** @property {TextureInfo} - The texture info for this sheet */
14290
+ this.textureInfo = new TextureInfo(this.canvas);
14291
+ /** @property {Vector2} - Where the next image will be packed */
14292
+ this.cursor = vec2();
14293
+ /** @property {number} - Height of the row being packed */
14294
+ this.rowHeight = 0;
14295
+ /** @property {boolean} - Has the canvas changed since the last webgl upload? */
14296
+ this.glDirty = false;
14297
+
14298
+ if (headlessMode)
14299
+ {
14300
+ // tiles still need bounds when there is no canvas to measure
14301
+ this.textureInfo.size = vec2(size);
14302
+ this.textureInfo.sizeInverse = vec2(1/size);
14303
+ }
14304
+ }
14305
+
14306
+ /** Find a spot for an image on this sheet without drawing it
14307
+ * @param {Vector2} imageSize - Size of the source image in pixels
14308
+ * @param {Vector2} [frameSize] - Size of each frame, or the whole image if not passed
14309
+ * @param {number} [padding] - How many pixels padding around each frame
14310
+ * @param {number} [sourcePadding] - How many pixels padding around each frame in the source image
14311
+ * @return {TileInfo} Tile for the packed image, or undefined if the sheet is full */
14312
+ tryAdd(imageSize, frameSize=imageSize, padding=textureSheetPadding, sourcePadding=0)
14313
+ {
14314
+ ASSERT(isVector2(imageSize) && isVector2(frameSize), 'sizes must be vec2');
14315
+ ASSERT(frameSize.x > 0 && frameSize.y > 0, 'frame size must be positive');
14316
+ ASSERT(isNumber(sourcePadding) && sourcePadding >= 0, 'sourcePadding must be a number >= 0');
14317
+
14318
+ // the source may have its own padding baked in around each frame
14319
+ const sourceCellWidth = frameSize.x + sourcePadding*2;
14320
+ const sourceCellHeight = frameSize.y + sourcePadding*2;
14321
+ ASSERT(imageSize.x % sourceCellWidth === 0 && imageSize.y % sourceCellHeight === 0,
14322
+ 'image size must be a multiple of the padded frame size');
14323
+
14324
+ const cellWidth = frameSize.x + padding*2;
14325
+ const cellHeight = frameSize.y + padding*2;
14326
+ const maxColumns = this.size / cellWidth | 0;
14327
+ ASSERT(maxColumns > 0, 'frame is too wide to fit on a texture sheet');
14328
+
14329
+ // keep the layout of the source image, but narrow it if a row is too wide
14330
+ // frames wrap down to the next row, which TileInfo.frame handles via columns
14331
+ const sourceColumns = imageSize.x / sourceCellWidth;
14332
+ const frameCount = sourceColumns * (imageSize.y / sourceCellHeight);
14333
+ const columns = min(sourceColumns, maxColumns);
14334
+ const blockWidth = columns * cellWidth;
14335
+ const blockHeight = ceil(frameCount / columns) * cellHeight;
14336
+
14337
+ // probe the placement using locals so a failed try leaves the sheet unchanged
14338
+ let x = this.cursor.x, y = this.cursor.y, rowHeight = this.rowHeight;
14339
+ if (x + blockWidth > this.size)
14340
+ {
14341
+ // start a new row if this one does not have enough space left
14342
+ x = 0;
14343
+ y += rowHeight;
14344
+ rowHeight = 0;
14345
+ }
14346
+
14347
+ // out of space, the caller needs to use a different sheet
14348
+ if (y + blockHeight > this.size)
14349
+ return undefined;
14350
+
14351
+ // commit the placement, tile pos points inside the padding to match how tile() works
14352
+ this.cursor.x = x + blockWidth;
14353
+ this.cursor.y = y;
14354
+ this.rowHeight = max(rowHeight, blockHeight);
14355
+ return new TileInfo(vec2(x + padding, y + padding), frameSize, this.textureInfo, padding, 0, columns);
14356
+ }
14357
+
14358
+ /** Draw an image into this sheet at a tile returned by tryAdd
14359
+ * @param {HTMLImageElement} image - Source image to copy from
14360
+ * @param {TileInfo} tileInfo - Where to put it, from tryAdd
14361
+ * @param {boolean} [update] - Upload to webgl now, pass false when batching
14362
+ * @param {number} [sourcePadding] - How many pixels padding around each frame in the source image */
14363
+ drawImage(image, tileInfo, update=true, sourcePadding=0)
14364
+ {
14365
+ ASSERT(!!this.context, 'texture sheet has no canvas');
14366
+
14367
+ // copy frames in order, reading the source left to right, top to bottom
14368
+ // the destination wraps at tileInfo.columns which may be narrower than the source
14369
+ const frameSize = tileInfo.size;
14370
+ const sourceCellWidth = frameSize.x + sourcePadding*2;
14371
+ const sourceCellHeight = frameSize.y + sourcePadding*2;
14372
+ const sourceColumns = image.width / sourceCellWidth;
14373
+ const frameCount = sourceColumns * (image.height / sourceCellHeight);
14374
+ const columns = tileInfo.columns || frameCount;
14375
+ const cellWidth = frameSize.x + tileInfo.padding*2;
14376
+ const cellHeight = frameSize.y + tileInfo.padding*2;
14377
+ for (let i = frameCount; i--;)
14378
+ {
14379
+ const sourceX = (i % sourceColumns) * sourceCellWidth + sourcePadding;
14380
+ const sourceY = (i / sourceColumns | 0) * sourceCellHeight + sourcePadding;
14381
+ this.context.drawImage(image,
14382
+ sourceX, sourceY, frameSize.x, frameSize.y,
14383
+ tileInfo.pos.x + (i % columns) * cellWidth,
14384
+ tileInfo.pos.y + (i / columns | 0) * cellHeight,
14385
+ frameSize.x, frameSize.y);
14386
+ }
14387
+
14388
+ // upload now unless the caller is batching more images
14389
+ this.glDirty = true;
14390
+ update && this.updateTexture();
14391
+ }
14392
+
14393
+ /** Upload the canvas to webgl if it has changed since the last upload
14394
+ * Only needed after batching, drawImage uploads automatically by default */
14395
+ updateTexture()
14396
+ {
14397
+ if (!this.glDirty) return;
14398
+ this.glDirty = false;
14399
+ this.textureInfo.createWebGLTexture();
14400
+ }
14401
+ }
14402
+
14403
+ ///////////////////////////////////////////////////////////////////////////////
14404
+
14405
+ /** Load an image and pack it into a texture sheet
14406
+ * - Returns a TileInfo immediately which is filled in when the image loads
14407
+ * - Nothing is visible until it loads, use spritesReady to wait for it
14408
+ * - Pass frameSize for animations, then step through them with TileInfo.frame
14409
+ * - Grid images keep their layout and frames wrap down to the next row
14410
+ * - Pass sourcePadding if the source image has padding baked in around frames
14411
+ * @param {string} src - Image source path
14412
+ * @param {Vector2|number} [frameSize] - Size of each animation frame in pixels
14413
+ * @param {number} [padding] - How many pixels padding around each frame
14414
+ * @param {number} [sourcePadding] - How many pixels padding around each frame in the source image
14415
+ * @return {TileInfo}
14416
+ * @example
14417
+ * const playerTile = loadSprite('player.png'); // a single sprite
14418
+ * const runTile = loadSprite('run.png', vec2(16)); // a 16x16 frame animation
14419
+ * @memberof TextureSheets */
14420
+ function loadSprite(src, frameSize, padding=textureSheetPadding, sourcePadding=0)
14421
+ {
14422
+ ASSERT(isStringLike(src), 'image src must be a string');
14423
+ ASSERT(!frameSize || isVector2(frameSize) || isNumber(frameSize), 'frameSize must be a vec2 or number');
14424
+ ASSERT(isNumber(padding), 'padding must be a number');
14425
+
14426
+ if (isNumber(frameSize))
14427
+ frameSize = vec2(frameSize);
14428
+
14429
+ // start with an empty tile that gets filled in when the image loads
14430
+ const tileInfo = new TileInfo(vec2(), vec2(), undefined, padding, 0);
14431
+ if (headlessMode) return tileInfo;
14432
+
14433
+ // point at a sheet right away so drawing before it loads picks up empty pixels
14434
+ tileInfo.textureInfo = (textureSheets[0] || textureSheetCreate()).textureInfo;
14435
+
14436
+ // start decoding right away, images decode in parallel
14437
+ const image = new Image;
14438
+ const imagePromise = new Promise(resolve =>
14439
+ {
14440
+ image.onerror = image.onload = resolve;
14441
+ image.crossOrigin = 'anonymous';
14442
+ image.src = src;
14443
+ });
14444
+
14445
+ // pack through a queue so sheets fill in call order, not decode order
14446
+ ++textureSheetPendingCount;
14447
+ textureSheetQueue = textureSheetQueue.then(async ()=>
14448
+ {
14449
+ await imagePromise;
14450
+ if (image.width)
14451
+ {
14452
+ // pack onto a sheet, then fill in the tile that was already handed out,
14453
+ // copying every field so nothing is missed if TileInfo gains more of them
14454
+ const imageSize = vec2(image.width, image.height);
14455
+ const {sheet, tile} = textureSheetAdd(imageSize, frameSize, padding, sourcePadding);
14456
+ Object.assign(tileInfo, tile);
14457
+ sheet.drawImage(image, tileInfo, false, sourcePadding); // upload once per batch below
14458
+ }
14459
+ else
14460
+ {
14461
+ // leave the tile empty if the image failed to load
14462
+ LOG('loadSprite failed to load image:', src);
14463
+ }
14464
+
14465
+ // upload to webgl once per batch, when the last pending load finishes
14466
+ if (!--textureSheetPendingCount)
14467
+ textureSheets.forEach(s=> s.updateTexture());
14468
+ });
14469
+
14470
+ return tileInfo;
14471
+ }
14472
+
14473
+ /** Load a pre-packed texture atlas and repack it onto texture sheets
14474
+ * - Supports TexturePacker json (hash and array) and Aseprite json
14475
+ * - Returns an empty object which is filled with TileInfos when loaded
14476
+ * - Frames are named by the json, animations are grouped automatically
14477
+ * - Aseprite frame tags become animations, so do names like run_0, run_1
14478
+ * - Trimmed frames are restored to their full source size when packed
14479
+ * - Rotated frames are rotated back upright when packed
14480
+ * @param {string} imageSrc - Atlas image path
14481
+ * @param {string|Object} jsonSrc - Atlas json path, or already parsed json data
14482
+ * @param {number} [padding] - How many pixels padding around each frame
14483
+ * @return {Object} Object mapping frame and animation names to TileInfos
14484
+ * @example
14485
+ * const atlas = loadAtlas('sprites.png', 'sprites.json');
14486
+ * await spritesReady();
14487
+ * drawTile(pos, size, atlas.player); // a single frame
14488
+ * drawTile(pos, size, atlas.run.frame(2)); // frame 2 of the run animation
14489
+ * @memberof TextureSheets */
14490
+ function loadAtlas(imageSrc, jsonSrc, padding=textureSheetPadding)
14491
+ {
14492
+ ASSERT(isStringLike(imageSrc), 'atlas image src must be a string');
14493
+ ASSERT(isStringLike(jsonSrc) || typeof jsonSrc === 'object', 'atlas json must be a path or object');
14494
+ ASSERT(isNumber(padding), 'padding must be a number');
14495
+
14496
+ const atlas = {};
14497
+ if (headlessMode) return atlas;
14498
+
14499
+ // start fetching the json and decoding the image right away, in parallel
14500
+ const jsonPromise = typeof jsonSrc === 'object' ? Promise.resolve(jsonSrc) :
14501
+ fetch(jsonSrc).then(r=> r.ok && r.json()).catch(()=> undefined);
14502
+ const image = new Image;
14503
+ const imagePromise = new Promise(resolve =>
14504
+ {
14505
+ image.onerror = image.onload = resolve;
14506
+ image.crossOrigin = 'anonymous';
14507
+ image.src = imageSrc;
14508
+ });
14509
+
14510
+ // pack through a queue so sheets fill in call order, not decode order
14511
+ ++textureSheetPendingCount;
14512
+ textureSheetQueue = textureSheetQueue.then(async ()=>
14513
+ {
14514
+ const data = await jsonPromise;
14515
+ await imagePromise;
14516
+ if (image.width && data)
14517
+ {
14518
+ for (const group of parseAtlas(data))
14519
+ {
14520
+ // reserve a block of full size cells, one per frame
14521
+ const sourceSize = group.frames[0].sourceSize;
14522
+ const blockSize = vec2(sourceSize.x*group.frames.length, sourceSize.y);
14523
+ const {sheet, tile} = textureSheetAdd(blockSize, sourceSize, padding);
14524
+
14525
+ // draw each frame untrimmed into its cell
14526
+ const context = sheet.context;
14527
+ const cellWidth = sourceSize.x + padding*2;
14528
+ const cellHeight = sourceSize.y + padding*2;
14529
+ group.frames.forEach((f, i)=>
14530
+ {
14531
+ const x = tile.pos.x + (i % tile.columns)*cellWidth + f.offset.x;
14532
+ const y = tile.pos.y + (i / tile.columns |0)*cellHeight + f.offset.y;
14533
+ if (f.rotated)
14534
+ {
14535
+ // stored rotated 90 degrees clockwise, draw it back upright
14536
+ context.save();
14537
+ context.translate(x, y);
14538
+ context.rotate(-PI/2);
14539
+ context.drawImage(image, f.pos.x, f.pos.y, f.size.y, f.size.x,
14540
+ -f.size.y, 0, f.size.y, f.size.x);
14541
+ context.restore();
14542
+ }
14543
+ else
14544
+ context.drawImage(image, f.pos.x, f.pos.y, f.size.x, f.size.y,
14545
+ x, y, f.size.x, f.size.y);
14546
+ });
14547
+ sheet.glDirty = true;
14548
+ atlas[group.name] = tile;
14549
+ }
14550
+ }
14551
+ else
14552
+ {
14553
+ // leave the atlas empty if either file failed to load
14554
+ LOG('loadAtlas failed to load:', imageSrc, jsonSrc);
14555
+ }
14556
+
14557
+ // upload to webgl once per batch, when the last pending load finishes
14558
+ if (!--textureSheetPendingCount)
14559
+ textureSheets.forEach(s=> s.updateTexture());
14560
+ });
14561
+
14562
+ return atlas;
14563
+ }
14564
+
14565
+ /** Parse atlas json into a list of named frame groups, used by loadAtlas
14566
+ * - Accepts TexturePacker json (hash and array) and Aseprite json
14567
+ * - Frames tagged in Aseprite or named like run_0, run_1 group into animations
14568
+ * @param {Object} data - Parsed atlas json data
14569
+ * @return {Array<Object>} List of {name, frames} groups in atlas order
14570
+ * @memberof TextureSheets */
14571
+ function parseAtlas(data)
14572
+ {
14573
+ ASSERT(!!data?.frames, 'unrecognized atlas format, expected TexturePacker or Aseprite json');
14574
+
14575
+ // normalize both hash and array frame layouts into a single list
14576
+ const frames = (isArray(data.frames) ?
14577
+ data.frames.map(f=> [f.filename, f]) : Object.entries(data.frames))
14578
+ .map(([name, f])=> ({
14579
+ name: name.replace(/\.[^.\\/]+$/, ''), // strip file extension
14580
+ pos: vec2(f.frame.x, f.frame.y),
14581
+ size: vec2(f.frame.w, f.frame.h),
14582
+ offset: vec2(f.spriteSourceSize?.x ?? 0, f.spriteSourceSize?.y ?? 0),
14583
+ sourceSize: vec2(f.sourceSize?.w ?? f.frame.w, f.sourceSize?.h ?? f.frame.h),
14584
+ rotated: !!f.rotated,
14585
+ }));
14586
+
14587
+ const groups = [];
14588
+ const tags = data.meta?.frameTags;
14589
+ if (tags?.length)
14590
+ {
14591
+ // aseprite tags are authoritative, untagged frames stay individual
14592
+ const tagged = new Set;
14593
+ for (const tag of tags)
14594
+ {
14595
+ groups.push({name: tag.name, frames: frames.slice(tag.from, tag.to + 1)});
14596
+ for (let i = tag.from; i <= tag.to; ++i)
14597
+ tagged.add(i);
14598
+ }
14599
+ frames.forEach((f, i)=> tagged.has(i) || groups.push({name: f.name, frames: [f]}));
14600
+ return groups;
14601
+ }
14602
+
14603
+ // group frames that share a name stem with contiguous trailing numbers
14604
+ // run_0.png and run_1.png become a 2 frame animation named run
14605
+ const stems = new Map;
14606
+ for (const f of frames)
14607
+ {
14608
+ let match = f.name.match(/^(.+?)([-_ ])?(\d+)$/);
14609
+ if (match && !match[2] && /\d$/.test(match[1]))
14610
+ match = undefined; // all digit tails like 10 are a name, not frame 0 of 1
14611
+ const stem = match ? match[1] : f.name;
14612
+ f.groupIndex = match ? Number(match[3]) : undefined;
14613
+ stems.has(stem) || stems.set(stem, []);
14614
+ stems.get(stem).push(f);
14615
+ }
14616
+ for (const [stem, list] of stems)
14617
+ {
14618
+ // only group 2 or more frames with contiguous indices and matching sizes
14619
+ list.sort((a, b)=> a.groupIndex - b.groupIndex);
14620
+ const grouped = list.length > 1 &&
14621
+ list.every((f, i)=> f.groupIndex === list[0].groupIndex + i) &&
14622
+ list.every(f=> f.sourceSize.x === list[0].sourceSize.x &&
14623
+ f.sourceSize.y === list[0].sourceSize.y);
14624
+ if (grouped)
14625
+ groups.push({name: stem, frames: list});
14626
+ else
14627
+ list.forEach(f=> groups.push({name: f.name, frames: [f]}));
14628
+ }
14629
+ return groups;
14630
+ }
14631
+
14632
+ /** Wait for everything started by loadSprite and loadAtlas to finish packing
14633
+ * @return {Promise}
14634
+ * @example
14635
+ * async function gameInit()
14636
+ * {
14637
+ * playerTile = loadSprite('player.png');
14638
+ * runTile = loadSprite('run.png', vec2(16));
14639
+ * await spritesReady();
14640
+ * }
14641
+ * @memberof TextureSheets */
14642
+ async function spritesReady()
14643
+ {
14644
+ // keep waiting until the queue drains, more sprites may load while waiting
14645
+ while (textureSheetPendingCount)
14646
+ await textureSheetQueue;
14647
+ }
14648
+
14649
+ // create a new texture sheet and add it to the list
14650
+ function textureSheetCreate()
14651
+ {
14652
+ const sheet = new TextureSheet;
14653
+ textureSheets.push(sheet);
14654
+ return sheet;
14655
+ }
14656
+
14657
+ // use the first sheet with enough space, or make a new one
14658
+ function textureSheetAdd(imageSize, frameSize, padding, sourcePadding)
14659
+ {
14660
+ let sheet, tile;
14661
+ for (sheet of textureSheets)
14662
+ if (tile = sheet.tryAdd(imageSize, frameSize, padding, sourcePadding))
14663
+ break;
14664
+ if (!tile)
14665
+ {
14666
+ sheet = textureSheetCreate();
14667
+ tile = sheet.tryAdd(imageSize, frameSize, padding, sourcePadding);
14668
+ ASSERT(!!tile, 'image is too large to fit on a texture sheet');
14669
+ }
14670
+ return {sheet, tile};
14671
+ }
14672
+
14673
+ ///////////////////////////////////////////////////////////////////////////////
14674
+ // Texture sheet setting setters
14675
+
14676
+ /** Set width and height in pixels of texture sheets created by loadSprite
14677
+ * @param {number} size
14678
+ * @memberof Settings */
14679
+ function setTextureSheetSize(size) { textureSheetSize = size; }
14680
+
14681
+ /** Set default padding pixels around each frame packed by loadSprite
14682
+ * @param {number} padding
14683
+ * @memberof Settings */
14684
+ function setTextureSheetPadding(padding) { textureSheetPadding = padding; }
14685
+
14219
14686
  /**
14220
14687
  * LittleJS Tween System Plugin
14221
14688
  * - Lightweight tweens for numbers, Vector2, Color, or any .lerp-able type
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "littlejsengine",
3
- "version": "1.18.21",
3
+ "version": "1.18.23",
4
4
  "description": "LittleJS - Tiny and Fast HTML5 Game Engine",
5
5
  "main": "dist/littlejs.esm.js",
6
6
  "types": "dist/littlejs.d.ts",
@@ -100,4 +100,16 @@ export
100
100
  threeJS,
101
101
  ThreeJSPlugin,
102
102
  ThreeJSObject,
103
- }
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
+ }