- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
- 67
- 68
- 69
- 70
- 71
- 72
- 73
- 74
- 75
- 76
- 77
- 78
- 79
- 80
- 81
- 82
- 83
- 84
- 85
- 86
- 87
- 88
- 89
- 90
- 91
- 92
- 93
- 94
- 95
- 96
- 97
- 98
- 99
- 100
- 101
- 102
- 103
- 104
- 105
- 106
- 107
- 108
- 109
- 110
- 111
- 112
- 113
- 114
- 115
- 116
- 117
- 118
- 119
- 120
- 121
- 122
- 123
- 124
- 125
- 126
- 127
- 128
- 129
- 130
- 131
- 132
- 133
- 134
- 135
- 136
- 137
- 138
- 139
- 140
- 141
- 142
- 143
- 144
- 145
- 146
- 147
- 148
- 149
- 150
- 151
- 152
- 153
- 154
- 155
- 156
- 157
- 158
- 159
- 160
- 161
- 162
- 163
- 164
- 165
- 166
- 167
- 168
- 169
- 170
- 171
- 172
- 173
- 174
- 175
- 176
- 177
- 178
- 179
- 180
- 181
- 182
- 183
- 184
- 185
- 186
- 187
- 188
- 189
- 190
- 191
- 192
- 193
- 194
- 195
- 196
- 197
- 198
- 199
- 200
- 201
- 202
- 203
- 204
- 205
- 206
- 207
- 208
- 209
- 210
- 211
- 212
- 213
- 214
- 215
- 216
- 217
- 218
- 219
- 220
- 221
- 222
- 223
- 224
- 225
- 226
- 227
- 228
- 229
- 230
- 231
- 232
- 233
- 234
- 235
- 236
- 237
- 238
- 239
- 240
- 241
- 242
- 243
- 244
- 245
- 246
- 247
- 248
- 249
- 250
- 251
- 252
- 253
- 254
- 255
- 256
- 257
- 258
- 259
- 260
- 261
- 262
- 263
- 264
- 265
- 266
- 267
- 268
- 269
- 270
- 271
- 272
- 273
- 274
- 275
- 276
- 277
- 278
- 279
- 280
- 281
- 282
- 283
- 284
- 285
- 286
- 287
- 288
- 289
- 290
- 291
- 292
- 293
- 294
- 295
- 296
- 297
- 298
- 299
- 300
- 301
- 302
- 303
- 304
- 305
- 306
- 307
- 308
- 309
- 310
- 311
- 312
- 313
- 314
- 315
- 316
- 317
- 318
- 319
- 320
- 321
- 322
- 323
- 324
- 325
- 326
- 327
- 328
- 329
- 330
- 331
- 332
- 333
- 334
- 335
- 336
- 337
- 338
- 339
- 340
- 341
- 342
- 343
- 344
- 345
- 346
- 347
- 348
- 349
- 350
- 351
- 352
- 353
- 354
- 355
- 356
- 357
- 358
- 359
- 360
- 361
- 362
- 363
- 364
- 365
- 366
- 367
- 368
- 369
- 370
- 371
- 372
- 373
- 374
- 375
- 376
- 377
- 378
- 379
- 380
- 381
- 382
- 383
- 384
- 385
- 386
- 387
- 388
- 389
- 390
- 391
- 392
- 393
- 394
- 395
- 396
import { Rectangle } from '@pixi/math';
import { Texture, BaseTexture } from '@pixi/core';
import { deprecation, getResolutionOfUrl } from '@pixi/utils';
import type { Dict } from '@pixi/utils';
import type { ImageResource } from '@pixi/core';
import type { IPointData } from '@pixi/math';
/** Represents the JSON data for a spritesheet atlas. */
export interface ISpritesheetFrameData
{
frame: {
x: number;
y: number;
w: number;
h: number;
};
trimmed?: boolean;
rotated?: boolean;
sourceSize?: {
w: number;
h: number;
};
spriteSourceSize?: {
x: number;
y: number;
};
anchor?: IPointData;
}
/** Atlas format. */
export interface ISpritesheetData
{
frames: Dict<ISpritesheetFrameData>;
animations?: Dict<string[]>;
meta: {
scale: string;
// eslint-disable-next-line camelcase
related_multi_packs?: string[];
};
}
/**
* Utility class for maintaining reference to a collection
* of Textures on a single Spritesheet.
*
* To access a sprite sheet from your code you may pass its JSON data file to Pixi's loader:
*
* ```js
* PIXI.Loader.shared.add("images/spritesheet.json").load(setup);
*
* function setup() {
* let sheet = PIXI.Loader.shared.resources["images/spritesheet.json"].spritesheet;
* ...
* }
* ```
*
* Alternately, you may circumvent the loader by instantiating the Spritesheet directly:
* ```js
* const sheet = new PIXI.Spritesheet(texture, spritesheetData);
* await sheet.parse();
* console.log('Spritesheet ready to use!');
* ```
*
* With the `sheet.textures` you can create Sprite objects,`sheet.animations` can be used to create an AnimatedSprite.
*
* Sprite sheets can be packed using tools like {@link https://codeandweb.com/texturepacker|TexturePacker},
* {@link https://renderhjs.net/shoebox/|Shoebox} or {@link https://github.com/krzysztof-o/spritesheet.js|Spritesheet.js}.
* Default anchor points (see {@link PIXI.Texture#defaultAnchor}) and grouping of animation sprites are currently only
* supported by TexturePacker.
* @memberof PIXI
*/
export class Spritesheet
{
/** The maximum number of Textures to build per process. */
static readonly BATCH_SIZE = 1000;
/** For multi-packed spritesheets, this contains a reference to all the other spritesheets it depends on. */
public linkedSheets: Spritesheet[] = [];
/** Reference to ths source texture. */
public baseTexture: BaseTexture;
/**
* A map containing all textures of the sprite sheet.
* Can be used to create a {@link PIXI.Sprite|Sprite}:
* ```js
* new PIXI.Sprite(sheet.textures["image.png"]);
* ```
*/
public textures: Dict<Texture>;
/**
* A map containing the textures for each animation.
* Can be used to create an {@link PIXI.AnimatedSprite|AnimatedSprite}:
* ```js
* new PIXI.AnimatedSprite(sheet.animations["anim_name"])
* ```
*/
public animations: Dict<Texture[]>;
/**
* Reference to the original JSON data.
* @type {object}
*/
public data: ISpritesheetData;
/** The resolution of the spritesheet. */
public resolution: number;
/**
* Reference to original source image from the Loader. This reference is retained so we
* can destroy the Texture later on. It is never used internally.
*/
private _texture: Texture;
/**
* Map of spritesheet frames.
* @type {object}
*/
private _frames: Dict<ISpritesheetFrameData>;
/** Collection of frame names. */
private _frameKeys: string[];
/** Current batch index being processed. */
private _batchIndex: number;
/**
* Callback when parse is completed.
* @type {Function}
*/
private _callback: (textures: Dict<Texture>) => void;
/**
* @param texture - Reference to the source BaseTexture object.
* @param {object} data - Spritesheet image data.
* @param resolutionFilename - The filename to consider when determining
* the resolution of the spritesheet. If not provided, the imageUrl will
* be used on the BaseTexture.
*/
constructor(texture: BaseTexture | Texture, data: ISpritesheetData, resolutionFilename: string = null)
{
this._texture = texture instanceof Texture ? texture : null;
this.baseTexture = texture instanceof BaseTexture ? texture : this._texture.baseTexture;
this.textures = {};
this.animations = {};
this.data = data;
const resource = this.baseTexture.resource as ImageResource;
this.resolution = this._updateResolution(resolutionFilename || (resource ? resource.url : null));
this._frames = this.data.frames;
this._frameKeys = Object.keys(this._frames);
this._batchIndex = 0;
this._callback = null;
}
/**
* Generate the resolution from the filename or fallback
* to the meta.scale field of the JSON data.
* @param resolutionFilename - The filename to use for resolving
* the default resolution.
* @returns Resolution to use for spritesheet.
*/
private _updateResolution(resolutionFilename: string = null): number
{
const { scale } = this.data.meta;
// Use a defaultValue of `null` to check if a url-based resolution is set
let resolution = getResolutionOfUrl(resolutionFilename, null);
// No resolution found via URL
if (resolution === null)
{
// Use the scale value or default to 1
resolution = scale !== undefined ? parseFloat(scale) : 1;
}
// For non-1 resolutions, update baseTexture
if (resolution !== 1)
{
this.baseTexture.setResolution(resolution);
}
return resolution;
}
/**
* Parser spritesheet from loaded data. This is done asynchronously
* to prevent creating too many Texture within a single process.
* @method PIXI.Spritesheet#parse
*/
public parse(): Promise<Dict<Texture>>;
/**
* Please use the Promise-based version of this function.
* @method PIXI.Spritesheet#parse
* @deprecated since version 6.5.0
* @param {Function} callback - Callback when complete returns
* a map of the Textures for this spritesheet.
*/
public parse(callback?: (textures?: Dict<Texture>) => void): void;
/** @ignore */
public parse(callback?: (textures?: Dict<Texture>) => void): Promise<Dict<Texture>>
{
// #if _DEBUG
if (callback)
{
deprecation('6.5.0', 'Spritesheet.parse callback is deprecated, use the return Promise instead.');
}
// #endif
return new Promise((resolve) =>
{
this._callback = (textures: Dict<Texture>) =>
{
callback?.(textures);
resolve(textures);
};
this._batchIndex = 0;
if (this._frameKeys.length <= Spritesheet.BATCH_SIZE)
{
this._processFrames(0);
this._processAnimations();
this._parseComplete();
}
else
{
this._nextBatch();
}
});
}
/**
* Process a batch of frames
* @param initialFrameIndex - The index of frame to start.
*/
private _processFrames(initialFrameIndex: number): void
{
let frameIndex = initialFrameIndex;
const maxFrames = Spritesheet.BATCH_SIZE;
while (frameIndex - initialFrameIndex < maxFrames && frameIndex < this._frameKeys.length)
{
const i = this._frameKeys[frameIndex];
const data = this._frames[i];
const rect = data.frame;
if (rect)
{
let frame = null;
let trim = null;
const sourceSize = data.trimmed !== false && data.sourceSize
? data.sourceSize : data.frame;
const orig = new Rectangle(
0,
0,
Math.floor(sourceSize.w) / this.resolution,
Math.floor(sourceSize.h) / this.resolution
);
if (data.rotated)
{
frame = new Rectangle(
Math.floor(rect.x) / this.resolution,
Math.floor(rect.y) / this.resolution,
Math.floor(rect.h) / this.resolution,
Math.floor(rect.w) / this.resolution
);
}
else
{
frame = new Rectangle(
Math.floor(rect.x) / this.resolution,
Math.floor(rect.y) / this.resolution,
Math.floor(rect.w) / this.resolution,
Math.floor(rect.h) / this.resolution
);
}
// Check to see if the sprite is trimmed
if (data.trimmed !== false && data.spriteSourceSize)
{
trim = new Rectangle(
Math.floor(data.spriteSourceSize.x) / this.resolution,
Math.floor(data.spriteSourceSize.y) / this.resolution,
Math.floor(rect.w) / this.resolution,
Math.floor(rect.h) / this.resolution
);
}
this.textures[i] = new Texture(
this.baseTexture,
frame,
orig,
trim,
data.rotated ? 2 : 0,
data.anchor
);
// lets also add the frame to pixi's global cache for 'from' and 'fromLoader' functions
Texture.addToCache(this.textures[i], i);
}
frameIndex++;
}
}
/** Parse animations config. */
private _processAnimations(): void
{
const animations = this.data.animations || {};
for (const animName in animations)
{
this.animations[animName] = [];
for (let i = 0; i < animations[animName].length; i++)
{
const frameName = animations[animName][i];
this.animations[animName].push(this.textures[frameName]);
}
}
}
/** The parse has completed. */
private _parseComplete(): void
{
const callback = this._callback;
this._callback = null;
this._batchIndex = 0;
callback.call(this, this.textures);
}
/** Begin the next batch of textures. */
private _nextBatch(): void
{
this._processFrames(this._batchIndex * Spritesheet.BATCH_SIZE);
this._batchIndex++;
setTimeout(() =>
{
if (this._batchIndex * Spritesheet.BATCH_SIZE < this._frameKeys.length)
{
this._nextBatch();
}
else
{
this._processAnimations();
this._parseComplete();
}
}, 0);
}
/**
* Destroy Spritesheet and don't use after this.
* @param {boolean} [destroyBase=false] - Whether to destroy the base texture as well
*/
public destroy(destroyBase = false): void
{
for (const i in this.textures)
{
this.textures[i].destroy();
}
this._frames = null;
this._frameKeys = null;
this.data = null;
this.textures = null;
if (destroyBase)
{
this._texture?.destroy();
this.baseTexture.destroy();
}
this._texture = null;
this.baseTexture = null;
this.linkedSheets = [];
}
}
/**
* Reference to Spritesheet object created.
* @member {PIXI.Spritesheet} spritesheet
* @memberof PIXI.LoaderResource
* @instance
*/
/**
* Dictionary of textures from Spritesheet.
* @member {Object<string, PIXI.Texture>} textures
* @memberof PIXI.LoaderResource
* @instance
*/