- 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
- 397
- 398
- 399
- 400
- 401
- 402
- 403
- 404
- 405
- 406
- 407
- 408
- 409
- 410
- 411
- 412
- 413
- 414
- 415
- 416
- 417
- 418
- 419
- 420
- 421
- 422
- 423
- 424
- 425
- 426
- 427
- 428
- 429
- 430
- 431
- 432
- 433
- 434
- 435
- 436
- 437
- 438
- 439
- 440
- 441
- 442
- 443
- 444
- 445
- 446
- 447
- 448
- 449
- 450
- 451
- 452
- 453
- 454
- 455
- 456
- 457
- 458
- 459
- 460
- 461
- 462
- 463
- 464
- 465
- 466
- 467
- 468
- 469
- 470
- 471
- 472
- 473
- 474
- 475
- 476
- 477
- 478
- 479
- 480
- 481
- 482
- 483
- 484
- 485
- 486
- 487
- 488
- 489
- 490
- 491
- 492
- 493
- 494
- 495
- 496
- 497
- 498
- 499
- 500
- 501
- 502
- 503
- 504
- 505
- 506
- 507
- 508
- 509
- 510
- 511
- 512
- 513
- 514
- 515
- 516
- 517
- 518
- 519
- 520
- 521
- 522
- 523
- 524
- 525
- 526
- 527
- 528
- 529
- 530
- 531
- 532
- 533
- 534
- 535
- 536
- 537
- 538
- 539
- 540
- 541
- 542
- 543
- 544
- 545
import { path } from '@pixi/utils';
import { convertToList } from '../utils/convertToList';
import { createStringVariations } from '../utils/createStringVariations';
import { isSingleItem } from '../utils/isSingleItem';
import type { ResolveAsset, PreferOrder, ResolveURLParser, ResolverManifest, ResolverBundle } from './types';
/**
* A class that is responsible for resolving mapping asset URLs to keys.
* At its most basic it can be used for Aliases:
*
* ```
* resolver.add('foo', 'bar');
* resolver.resolveUrl('foo') // => 'bar'
* ```
*
* It can also be used to resolve the most appropriate asset for a given URL:
*
* ```
* resolver.prefer({
* params:{
* format:'webp',
* resolution: 2,
* }
* })
*
* resolver.add('foo', ['bar@2x.webp', 'bar@2x.png', 'bar.webp', 'bar.png']);
*
* resolver.resolveUrl('foo') // => 'bar@2x.webp'
* ```
* Other features include:
* - Ability to process a manifest file to get the correct understanding of how to resolve all assets
* - Ability to add custom parsers for specific file types
* - Ability to add custom prefer rules
*
* This class only cares about the URL, not the loading of the asset itself.
*
* It is not intended that this class is created by developers - its part of the Asset class
* This is the third major system of PixiJS' main Assets class
* @memberof PIXI
*/
export class Resolver
{
private _assetMap: Record<string, ResolveAsset[]> = {};
private _preferredOrder: PreferOrder[] = [];
private _parsers: ResolveURLParser[] = [];
private _resolverHash: Record<string, ResolveAsset> = {};
private _rootPath: string;
private _basePath: string;
private _manifest: ResolverManifest;
private _bundles: Record<string, string[]> = {};
/**
* Let the resolver know which assets you prefer to use when resolving assets.
* Multiple prefer user defined rules can be added.
* @example
* resolver.prefer({
* // first look for something with the correct format, and then then correct resolution
* priority: ['format', 'resolution'],
* params:{
* format:'webp', // prefer webp images
* resolution: 2, // prefer a resolution of 2
* }
* })
* resolver.add('foo', ['bar@2x.webp', 'bar@2x.png', 'bar.webp', 'bar.png']);
* resolver.resolveUrl('foo') // => 'bar@2x.webp'
* @param preferOrders - the prefer options
*/
public prefer(...preferOrders: PreferOrder[]): void
{
preferOrders.forEach((prefer) =>
{
this._preferredOrder.push(prefer);
if (!prefer.priority)
{
// generate the priority based on the order of the object
prefer.priority = Object.keys(prefer.params);
}
});
this._resolverHash = {};
}
/**
* Set the base path to prepend to all urls when resolving
* @example
* resolver.basePath = 'https://home.com/';
* resolver.add('foo', 'bar.ong');
* resolver.resolveUrl('foo', 'bar.png'); // => 'https://home.com/bar.png'
* @param basePath - the base path to use
*/
public set basePath(basePath: string)
{
this._basePath = basePath;
}
public get basePath(): string
{
return this._basePath;
}
/**
* Set the root path for root-relative URLs. By default the `basePath`'s root is used. If no `basePath` is set, then the
* default value for browsers is `window.location.origin`
* @example
* // Application hosted on https://home.com/some-path/index.html
* resolver.basePath = 'https://home.com/some-path/';
* resolver.rootPath = 'https://home.com/';
* resolver.add('foo', '/bar.png');
* resolver.resolveUrl('foo', '/bar.png'); // => 'https://home.com/bar.png'
* @param rootPath - the root path to use
*/
public set rootPath(rootPath: string)
{
this._rootPath = rootPath;
}
public get rootPath(): string
{
return this._rootPath;
}
/**
* All the active URL parsers that help the parser to extract information and create
* an asset object-based on parsing the URL itself.
*
* Can be added using the extensions API
* @example
* resolver.add('foo', [
* {
* resolution:2,
* format:'png'
* src: 'image@2x.png'
* },
* {
* resolution:1,
* format:'png'
* src: 'image.png'
* }
* ]);
*
* // with a url parser the information such as resolution and file format could extracted from the url itself:
* extensions.add({
* extension: ExtensionType.ResolveParser,
* test: loadTextures.test, // test if url ends in an image
* parse: (value: string) =>
* ({
* resolution: parseFloat(settings.RETINA_PREFIX.exec(value)?.[1] ?? '1'),
* format: value.split('.').pop(),
* src: value,
* }),
* });
*
* // now resolution and format can be extracted from the url
* resolver.add('foo', [
* 'image@2x.png'
* 'image.png'
* ]);
* @
*/
public get parsers(): ResolveURLParser[]
{
return this._parsers;
}
/** Used for testing, this resets the resolver to its initial state */
public reset(): void
{
this._preferredOrder = [];
this._resolverHash = {};
this._assetMap = {};
this._rootPath = null;
this._basePath = null;
this._manifest = null;
}
/**
* Add a manifest to the asset resolver. This is a nice way to add all the asset information in one go.
* generally a manifest would be built using a tool.
* @param manifest - the manifest to add to the resolver
*/
public addManifest(manifest: ResolverManifest): void
{
if (this._manifest)
{
// #if _DEBUG
console.warn('[Resolver] Manifest already exists, this will be overwritten');
// #endif
}
this._manifest = manifest;
manifest.bundles.forEach((bundle) =>
{
this.addBundle(bundle.name, bundle.assets);
});
}
/**
* This adds a bundle of assets in one go so that you can resolve them as a group.
* For example you could add a bundle for each screen in you pixi app
* @example
* resolver.addBundle('animals', {
* bunny: 'bunny.png',
* chicken: 'chicken.png',
* thumper: 'thumper.png',
* });
*
* const resolvedAssets = await resolver.resolveBundle('animals');
* @param bundleId - The id of the bundle to add
* @param assets - A record of the asset or assets that will be chosen from when loading via the specified key
*/
public addBundle(bundleId: string, assets: ResolverBundle['assets']): void
{
const assetNames: string[] = [];
if (Array.isArray(assets))
{
assets.forEach((asset) =>
{
if (typeof asset.name === 'string')
{
assetNames.push(asset.name);
}
else
{
assetNames.push(...asset.name);
}
this.add(asset.name, asset.srcs);
});
}
else
{
Object.keys(assets).forEach((key) =>
{
assetNames.push(key);
this.add(key, assets[key]);
});
}
this._bundles[bundleId] = assetNames;
}
/**
* Tells the resolver what keys are associated with witch asset.
* The most important thing the resolver does
* @example
* // single key, single asset:
* resolver.add('foo', 'bar.png');
* resolver.resolveUrl('foo') // => 'bar.png'
*
* // multiple keys, single asset:
* resolver.add(['foo', 'boo'], 'bar.png');
* resolver.resolveUrl('foo') // => 'bar.png'
* resolver.resolveUrl('boo') // => 'bar.png'
*
* // multiple keys, multiple assets:
* resolver.add(['foo', 'boo'], ['bar.png', 'bar.webp']);
* resolver.resolveUrl('foo') // => 'bar.png'
*
* // add custom data attached to the resolver
* Resolver.add(
* 'bunnyBooBooSmooth',
* 'bunny{png,webp}',
* {scaleMode:SCALE_MODES.NEAREST} // base texture options
* );
*
* resolver.resolve('bunnyBooBooSmooth') // => {src: 'bunny.png', data: {scaleMode: SCALE_MODES.NEAREST}}
* @param keysIn - The keys to map, can be an array or a single key
* @param assetsIn - The assets to associate with the key(s)
* @param data - The data that will be attached to the object that resolved object.
*/
public add(keysIn: string | string[], assetsIn: string | ResolveAsset | (ResolveAsset | string)[], data?: unknown): void
{
const keys: string[] = convertToList<string>(keysIn);
keys.forEach((key) =>
{
if (this._assetMap[key])
{
// #if _DEBUG
console.warn(`[Resolver] already has key: ${key} overwriting`);
// #endif
}
});
if (!Array.isArray(assetsIn))
{
if (typeof assetsIn === 'string')
{
assetsIn = createStringVariations(assetsIn);
}
else
{
assetsIn = [assetsIn];
}
}
const assetMap: ResolveAsset[] = assetsIn.map((asset): ResolveAsset =>
{
let formattedAsset = asset as ResolveAsset;
// check if is a string
if (typeof asset === 'string')
{
// first see if it contains any {} tags...
let parsed = false;
for (let i = 0; i < this._parsers.length; i++)
{
const parser = this._parsers[i];
if (parser.test(asset))
{
formattedAsset = parser.parse(asset);
parsed = true;
break;
}
}
if (!parsed)
{
formattedAsset = {
src: asset,
};
}
}
if (!formattedAsset.format)
{
formattedAsset.format = formattedAsset.src.split('.').pop();
}
if (!formattedAsset.alias)
{
formattedAsset.alias = keys;
}
if (this._basePath || this._rootPath)
{
formattedAsset.src = path.toAbsolute(formattedAsset.src, this._basePath, this._rootPath);
}
formattedAsset.data = formattedAsset.data ?? data;
return formattedAsset;
});
keys.forEach((key) =>
{
this._assetMap[key] = assetMap;
});
}
/**
* If the resolver has had a manifest set via setManifest, this will return the assets urls for
* a given bundleId or bundleIds.
* @example
* // manifest example
* const manifest = {
* bundles:[{
* name:'load-screen',
* assets:[
* {
* name: 'background',
* srcs: 'sunset.png',
* },
* {
* name: 'bar',
* srcs: 'load-bar.{png,webp}',
* }
* ]
* },
* {
* name:'game-screen',
* assets:[
* {
* name: 'character',
* srcs: 'robot.png',
* },
* {
* name: 'enemy',
* srcs: 'bad-guy.png',
* }
* ]
* }]
* }}
* resolver.setManifest(manifest);
* const resolved = resolver.resolveBundle('load-screen');
* @param bundleIds - The bundle ids to resolve
* @returns All the bundles assets or a hash of assets for each bundle specified
*/
public resolveBundle(bundleIds: string | string[]):
Record<string, ResolveAsset> | Record<string, Record<string, ResolveAsset>>
{
const singleAsset = isSingleItem(bundleIds);
bundleIds = convertToList<string>(bundleIds);
const out: Record<string, Record<string, ResolveAsset>> = {};
bundleIds.forEach((bundleId) =>
{
const assetNames = this._bundles[bundleId];
if (assetNames)
{
out[bundleId] = this.resolve(assetNames) as Record<string, ResolveAsset>;
}
});
return singleAsset ? out[bundleIds[0]] : out;
}
/**
* Does exactly what resolve does, but returns just the URL rather than the whole asset object
* @param key - The key or keys to resolve
* @returns - The URLs associated with the key(s)
*/
public resolveUrl(key: string | string[]): string | Record<string, string>
{
const result = this.resolve(key);
if (typeof key !== 'string')
{
const out: Record<string, string> = {};
for (const i in result)
{
out[i] = (result as Record<string, ResolveAsset>)[i].src;
}
return out;
}
return (result as ResolveAsset).src;
}
/**
* Resolves each key in the list to an asset object.
* Another key function of the resolver! After adding all the various key/asset pairs. this will run the logic
* of finding which asset to return based on any preferences set using the `prefer` function
* by default the same key passed in will be returned if nothing is matched by the resolver.
* @example
* resolver.add('boo', 'bunny.png');
*
* resolver.resolve('boo') // => {src:'bunny.png'}
*
* // will return the same string as no key was added for this value..
* resolver.resolve('another-thing.png') // => {src:'another-thing.png'}
* @param keys - key or keys to resolve
* @returns - the resolve asset or a hash of resolve assets for each key specified
*/
public resolve(keys: string | string[]): ResolveAsset | Record<string, ResolveAsset>
{
const singleAsset = isSingleItem(keys);
keys = convertToList<string>(keys);
const result: Record<string, ResolveAsset> = {};
keys.forEach((key) =>
{
if (!this._resolverHash[key])
{
if (this._assetMap[key])
{
let assets = this._assetMap[key];
const preferredOrder = this._getPreferredOrder(assets);
const bestAsset = assets[0];
preferredOrder?.priority.forEach((priorityKey) =>
{
preferredOrder.params[priorityKey].forEach((value: unknown) =>
{
const filteredAssets = assets.filter((asset) =>
{
if (asset[priorityKey])
{
return asset[priorityKey] === value;
}
return false;
});
if (filteredAssets.length)
{
assets = filteredAssets;
}
});
});
this._resolverHash[key] = (assets[0] ?? bestAsset);
}
else
{
let src = key;
if (this._basePath || this._rootPath)
{
src = path.toAbsolute(src, this._basePath, this._rootPath);
}
// if the resolver fails we just pass back the key assuming its a url
this._resolverHash[key] = {
src,
};
}
}
result[key] = this._resolverHash[key];
});
return singleAsset ? result[keys[0]] : result;
}
/**
* Internal function for figuring out what prefer criteria an asset should use.
* @param assets
*/
private _getPreferredOrder(assets: ResolveAsset[]): PreferOrder
{
for (let i = 0; i < assets.length; i++)
{
const asset = assets[0];
const preferred = this._preferredOrder.find((preference: PreferOrder) =>
preference.params.format.includes(asset.format));
if (preferred)
{
return preferred;
}
}
return this._preferredOrder[0];
}
}