pixi.js
Version:
<p align="center"> <a href="https://pixijs.com" target="_blank" rel="noopener noreferrer"> <img height="150" src="https://files.pixijs.download/branding/pixijs-logo-transparent-dark.svg?v=1" alt="PixiJS logo"> </a> </p> <br/> <p align="center">
730 lines (726 loc) • 25.8 kB
JavaScript
'use strict';
var Extensions = require('../extensions/Extensions.js');
var loadBitmapFont = require('../scene/text-bitmap/asset/loadBitmapFont.js');
var warn = require('../utils/logging/warn.js');
var BackgroundLoader = require('./BackgroundLoader.js');
var Cache = require('./cache/Cache.js');
var cacheTextureArray = require('./cache/parsers/cacheTextureArray.js');
var detectAvif = require('./detections/parsers/detectAvif.js');
var detectDefaults = require('./detections/parsers/detectDefaults.js');
var detectMp4 = require('./detections/parsers/detectMp4.js');
var detectOgv = require('./detections/parsers/detectOgv.js');
var detectWebm = require('./detections/parsers/detectWebm.js');
var detectWebp = require('./detections/parsers/detectWebp.js');
var Loader = require('./loader/Loader.js');
var loadJson = require('./loader/parsers/loadJson.js');
var loadTxt = require('./loader/parsers/loadTxt.js');
var loadWebFont = require('./loader/parsers/loadWebFont.js');
var loadSVG = require('./loader/parsers/textures/loadSVG.js');
var loadTextures = require('./loader/parsers/textures/loadTextures.js');
var loadVideoTextures = require('./loader/parsers/textures/loadVideoTextures.js');
var resolveJsonUrl = require('./resolver/parsers/resolveJsonUrl.js');
var resolveTextureUrl = require('./resolver/parsers/resolveTextureUrl.js');
var Resolver = require('./resolver/Resolver.js');
var convertToList = require('./utils/convertToList.js');
var isSingleItem = require('./utils/isSingleItem.js');
"use strict";
class AssetsClass {
constructor() {
this._detections = [];
this._initialized = false;
this.resolver = new Resolver.Resolver();
this.loader = new Loader.Loader();
this.cache = Cache.Cache;
this._backgroundLoader = new BackgroundLoader.BackgroundLoader(this.loader);
this._backgroundLoader.active = true;
this.reset();
}
/**
* Initializes the Assets class with configuration options. While not required,
* calling this before loading assets is recommended to set up default behaviors.
* @param options - Configuration options for the Assets system
* @example
* ```ts
* // Basic initialization (optional as Assets.load will call this automatically)
* await Assets.init();
*
* // With CDN configuration
* await Assets.init({
* basePath: 'https://my-cdn.com/assets/',
* defaultSearchParams: { version: '1.0.0' }
* });
*
* // With manifest and preferences
* await Assets.init({
* manifest: {
* bundles: [{
* name: 'game-screen',
* assets: [
* {
* alias: 'hero',
* src: 'hero.{png,webp}',
* data: { scaleMode: SCALE_MODES.NEAREST }
* },
* {
* alias: 'map',
* src: 'map.json'
* }
* ]
* }]
* },
* // Optimize for device capabilities
* texturePreference: {
* resolution: window.devicePixelRatio,
* format: ['webp', 'png']
* },
* // Set global preferences
* preferences: {
* crossOrigin: 'anonymous',
* }
* });
*
* // Load assets after initialization
* const heroTexture = await Assets.load('hero');
* ```
* @remarks
* - Can be called only once; subsequent calls will be ignored with a warning
* - Format detection runs automatically unless `skipDetections` is true
* - The manifest can be a URL to a JSON file or an inline object
* @see {@link AssetInitOptions} For all available initialization options
* @see {@link AssetsManifest} For manifest format details
*/
async init(options = {}) {
if (this._initialized) {
warn.warn("[Assets]AssetManager already initialized, did you load before calling this Assets.init()?");
return;
}
this._initialized = true;
if (options.defaultSearchParams) {
this.resolver.setDefaultSearchParams(options.defaultSearchParams);
}
if (options.basePath) {
this.resolver.basePath = options.basePath;
}
if (options.bundleIdentifier) {
this.resolver.setBundleIdentifier(options.bundleIdentifier);
}
if (options.manifest) {
let manifest = options.manifest;
if (typeof manifest === "string") {
manifest = await this.load(manifest);
}
this.resolver.addManifest(manifest);
}
const resolutionPref = options.texturePreference?.resolution ?? 1;
const resolution = typeof resolutionPref === "number" ? [resolutionPref] : resolutionPref;
const formats = await this._detectFormats({
preferredFormats: options.texturePreference?.format,
skipDetections: options.skipDetections,
detections: this._detections
});
this.resolver.prefer({
params: {
format: formats,
resolution
}
});
if (options.preferences) {
this.setPreferences(options.preferences);
}
}
/**
* Registers assets with the Assets resolver. This method maps keys (aliases) to asset sources,
* allowing you to load assets using friendly names instead of direct URLs.
* @param assets - The unresolved assets to add to the resolver
* @example
* ```ts
* // Basic usage - single asset
* Assets.add({
* alias: 'myTexture',
* src: 'assets/texture.png'
* });
* const texture = await Assets.load('myTexture');
*
* // Multiple aliases for the same asset
* Assets.add({
* alias: ['hero', 'player'],
* src: 'hero.png'
* });
* const hero1 = await Assets.load('hero');
* const hero2 = await Assets.load('player'); // Same texture
*
* // Multiple format support
* Assets.add({
* alias: 'character',
* src: 'character.{webp,png}' // Will choose best format
* });
* Assets.add({
* alias: 'character',
* src: ['character.webp', 'character.png'], // Explicitly specify formats
* });
*
* // With texture options
* Assets.add({
* alias: 'sprite',
* src: 'sprite.png',
* data: { scaleMode: 'nearest' }
* });
*
* // Multiple assets at once
* Assets.add([
* { alias: 'bg', src: 'background.png' },
* { alias: 'music', src: 'music.mp3' },
* { alias: 'spritesheet', src: 'sheet.json', data: { ignoreMultiPack: false } }
* ]);
* ```
* @remarks
* - Assets are resolved when loaded, not when added
* - Multiple formats use the best available format for the browser
* - Adding with same alias overwrites previous definition
* - The `data` property is passed to the asset loader
* @see {@link Resolver} For details on asset resolution
* @see {@link LoaderParser} For asset-specific data options
* @advanced
*/
add(assets) {
this.resolver.add(assets);
}
async load(urls, onProgress) {
if (!this._initialized) {
await this.init();
}
const singleAsset = isSingleItem.isSingleItem(urls);
const urlArray = convertToList.convertToList(urls).map((url) => {
if (typeof url !== "string") {
const aliases = this.resolver.getAlias(url);
if (aliases.some((alias) => !this.resolver.hasKey(alias))) {
this.add(url);
}
return Array.isArray(aliases) ? aliases[0] : aliases;
}
if (!this.resolver.hasKey(url))
this.add({ alias: url, src: url });
return url;
});
const resolveResults = this.resolver.resolve(urlArray);
const out = await this._mapLoadToResolve(resolveResults, onProgress);
return singleAsset ? out[urlArray[0]] : out;
}
/**
* Registers a bundle of assets that can be loaded as a group. Bundles are useful for organizing
* assets into logical groups, such as game levels or UI screens.
* @param bundleId - Unique identifier for the bundle
* @param assets - Assets to include in the bundle
* @example
* ```ts
* // Add a bundle using array format
* Assets.addBundle('animals', [
* { alias: 'bunny', src: 'bunny.png' },
* { alias: 'chicken', src: 'chicken.png' },
* { alias: 'thumper', src: 'thumper.png' },
* ]);
*
* // Add a bundle using object format
* Assets.addBundle('animals', {
* bunny: 'bunny.png',
* chicken: 'chicken.png',
* thumper: 'thumper.png',
* });
*
* // Add a bundle with advanced options
* Assets.addBundle('ui', [
* {
* alias: 'button',
* src: 'button.{webp,png}',
* data: { scaleMode: 'nearest' }
* },
* {
* alias: ['logo', 'brand'], // Multiple aliases
* src: 'logo.svg',
* data: { resolution: 2 }
* }
* ]);
*
* // Load the bundle
* await Assets.loadBundle('animals');
*
* // Use the loaded assets
* const bunny = Sprite.from('bunny');
* const chicken = Sprite.from('chicken');
* ```
* @remarks
* - Bundle IDs must be unique
* - Assets in bundles are not loaded until `loadBundle` is called
* - Bundles can be background loaded using `backgroundLoadBundle`
* - Assets in bundles can be loaded individually using their aliases
* @see {@link Assets.loadBundle} For loading bundles
* @see {@link Assets.backgroundLoadBundle} For background loading bundles
* @see {@link Assets.unloadBundle} For unloading bundles
* @see {@link AssetsManifest} For manifest format details
*/
addBundle(bundleId, assets) {
this.resolver.addBundle(bundleId, assets);
}
/**
* Loads a bundle or multiple bundles of assets. Bundles are collections of related assets
* that can be loaded together.
* @param bundleIds - Single bundle ID or array of bundle IDs to load
* @param onProgress - Optional callback for load progress (0.0 to 1.0)
* @returns Promise that resolves with the loaded bundle assets
* @example
* ```ts
* // Define bundles in your manifest
* const manifest = {
* bundles: [
* {
* name: 'load-screen',
* assets: [
* {
* alias: 'background',
* src: 'sunset.png',
* },
* {
* alias: 'bar',
* src: 'load-bar.{png,webp}', // use an array of individual assets
* },
* ],
* },
* {
* name: 'game-screen',
* assets: [
* {
* alias: 'character',
* src: 'robot.png',
* },
* {
* alias: 'enemy',
* src: 'bad-guy.png',
* },
* ],
* },
* ]
* };
*
* // Initialize with manifest
* await Assets.init({ manifest });
*
* // Or add bundles programmatically
* Assets.addBundle('load-screen', [...]);
* Assets.loadBundle('load-screen');
*
* // Load a single bundle
* await Assets.loadBundle('load-screen');
* const bg = Sprite.from('background'); // Uses alias from bundle
*
* // Load multiple bundles
* await Assets.loadBundle([
* 'load-screen',
* 'game-screen'
* ]);
*
* // Load with progress tracking
* await Assets.loadBundle('game-screen', (progress) => {
* console.log(`Loading: ${Math.round(progress * 100)}%`);
* });
* ```
* @remarks
* - Bundle assets are cached automatically
* - Bundles can be pre-loaded using `backgroundLoadBundle`
* - Assets in bundles can be accessed by their aliases
* - Progress callback receives values from 0.0 to 1.0
* @throws {Error} If the bundle ID doesn't exist in the manifest
* @see {@link Assets.addBundle} For adding bundles programmatically
* @see {@link Assets.backgroundLoadBundle} For background loading bundles
* @see {@link Assets.unloadBundle} For unloading bundles
* @see {@link AssetsManifest} For manifest format details
*/
async loadBundle(bundleIds, onProgress) {
if (!this._initialized) {
await this.init();
}
let singleAsset = false;
if (typeof bundleIds === "string") {
singleAsset = true;
bundleIds = [bundleIds];
}
const resolveResults = this.resolver.resolveBundle(bundleIds);
const out = {};
const keys = Object.keys(resolveResults);
let count = 0;
let total = 0;
const _onProgress = () => {
onProgress?.(++count / total);
};
const promises = keys.map((bundleId) => {
const resolveResult = resolveResults[bundleId];
total += Object.keys(resolveResult).length;
return this._mapLoadToResolve(resolveResult, _onProgress).then((resolveResult2) => {
out[bundleId] = resolveResult2;
});
});
await Promise.all(promises);
return singleAsset ? out[bundleIds[0]] : out;
}
/**
* Initiates background loading of assets. This allows assets to be loaded passively while other operations
* continue, making them instantly available when needed later.
*
* Background loading is useful for:
* - Preloading game levels while in a menu
* - Loading non-critical assets during gameplay
* - Reducing visible loading screens
* @param urls - Single URL/alias or array of URLs/aliases to load in the background
* @example
* ```ts
* // Basic background loading
* Assets.backgroundLoad('images/level2-assets.png');
*
* // Background load multiple assets
* Assets.backgroundLoad([
* 'images/sprite1.png',
* 'images/sprite2.png',
* 'images/background.png'
* ]);
*
* // Later, when you need the assets
* const textures = await Assets.load([
* 'images/sprite1.png',
* 'images/sprite2.png'
* ]); // Resolves immediately if background loading completed
* ```
* @remarks
* - Background loading happens one asset at a time to avoid blocking the main thread
* - Loading can be interrupted safely by calling `Assets.load()`
* - Assets are cached as they complete loading
* - No progress tracking is available for background loading
*/
async backgroundLoad(urls) {
if (!this._initialized) {
await this.init();
}
if (typeof urls === "string") {
urls = [urls];
}
const resolveResults = this.resolver.resolve(urls);
this._backgroundLoader.add(Object.values(resolveResults));
}
/**
* Initiates background loading of asset bundles. Similar to backgroundLoad but works with
* predefined bundles of assets.
*
* Perfect for:
* - Preloading level bundles during gameplay
* - Loading UI assets during splash screens
* - Preparing assets for upcoming game states
* @param bundleIds - Single bundle ID or array of bundle IDs to load in the background
* @example
* ```ts
* // Define bundles in your manifest
* await Assets.init({
* manifest: {
* bundles: [
* {
* name: 'home',
* assets: [
* {
* alias: 'background',
* src: 'images/home-bg.png',
* },
* {
* alias: 'logo',
* src: 'images/logo.png',
* }
* ]
* },
* {
* name: 'level-1',
* assets: [
* {
* alias: 'background',
* src: 'images/level1/bg.png',
* },
* {
* alias: 'sprites',
* src: 'images/level1/sprites.json'
* }
* ]
* }]
* }
* });
*
* // Load the home screen assets right away
* await Assets.loadBundle('home');
* showHomeScreen();
*
* // Start background loading while showing home screen
* Assets.backgroundLoadBundle('level-1');
*
* // When player starts level, load completes faster
* await Assets.loadBundle('level-1');
* hideHomeScreen();
* startLevel();
* ```
* @remarks
* - Bundle assets are loaded one at a time
* - Loading can be interrupted safely by calling `Assets.loadBundle()`
* - Assets are cached as they complete loading
* - Requires bundles to be registered via manifest or `addBundle`
* @see {@link Assets.addBundle} For adding bundles programmatically
* @see {@link Assets.loadBundle} For immediate bundle loading
* @see {@link AssetsManifest} For manifest format details
*/
async backgroundLoadBundle(bundleIds) {
if (!this._initialized) {
await this.init();
}
if (typeof bundleIds === "string") {
bundleIds = [bundleIds];
}
const resolveResults = this.resolver.resolveBundle(bundleIds);
Object.values(resolveResults).forEach((resolveResult) => {
this._backgroundLoader.add(Object.values(resolveResult));
});
}
/**
* Only intended for development purposes.
* This will wipe the resolver and caches.
* You will need to reinitialize the Asset
* @internal
*/
reset() {
this.resolver.reset();
this.loader.reset();
this.cache.reset();
this._initialized = false;
}
get(keys) {
if (typeof keys === "string") {
return Cache.Cache.get(keys);
}
const assets = {};
for (let i = 0; i < keys.length; i++) {
assets[i] = Cache.Cache.get(keys[i]);
}
return assets;
}
/**
* helper function to map resolved assets back to loaded assets
* @param resolveResults - the resolve results from the resolver
* @param onProgress - the progress callback
*/
async _mapLoadToResolve(resolveResults, onProgress) {
const resolveArray = [...new Set(Object.values(resolveResults))];
this._backgroundLoader.active = false;
const loadedAssets = await this.loader.load(resolveArray, onProgress);
this._backgroundLoader.active = true;
const out = {};
resolveArray.forEach((resolveResult) => {
const asset = loadedAssets[resolveResult.src];
const keys = [resolveResult.src];
if (resolveResult.alias) {
keys.push(...resolveResult.alias);
}
keys.forEach((key) => {
out[key] = asset;
});
Cache.Cache.set(keys, asset);
});
return out;
}
/**
* Unloads assets and releases them from memory. This method ensures proper cleanup of
* loaded assets when they're no longer needed.
* @param urls - Single URL/alias or array of URLs/aliases to unload
* @example
* ```ts
* // Unload a single asset
* await Assets.unload('images/sprite.png');
*
* // Unload using an alias
* await Assets.unload('hero'); // Unloads the asset registered with 'hero' alias
*
* // Unload multiple assets
* await Assets.unload([
* 'images/background.png',
* 'images/character.png',
* 'hero'
* ]);
*
* // Unload and handle creation of new instances
* await Assets.unload('hero');
* const newHero = await Assets.load('hero'); // Will load fresh from source
* ```
* @remarks
* > [!WARNING]
* > Make sure assets aren't being used before unloading:
* > - Remove sprites using the texture
* > - Clear any references to the asset
* > - Textures will be destroyed and can't be used after unloading
* @throws {Error} If the asset is not found in cache
*/
async unload(urls) {
if (!this._initialized) {
await this.init();
}
const urlArray = convertToList.convertToList(urls).map((url) => typeof url !== "string" ? url.src : url);
const resolveResults = this.resolver.resolve(urlArray);
await this._unloadFromResolved(resolveResults);
}
/**
* Unloads all assets in a bundle. Use this to free memory when a bundle's assets
* are no longer needed, such as when switching game levels.
* @param bundleIds - Single bundle ID or array of bundle IDs to unload
* @example
* ```ts
* // Define and load a bundle
* Assets.addBundle('level-1', {
* background: 'level1/bg.png',
* sprites: 'level1/sprites.json',
* music: 'level1/music.mp3'
* });
*
* // Load the bundle
* const level1 = await Assets.loadBundle('level-1');
*
* // Use the assets
* const background = Sprite.from(level1.background);
*
* // When done with the level, unload everything
* await Assets.unloadBundle('level-1');
* // background sprite is now invalid!
*
* // Unload multiple bundles
* await Assets.unloadBundle([
* 'level-1',
* 'level-2',
* 'ui-elements'
* ]);
* ```
* @remarks
* > [!WARNING]
* > - All assets in the bundle will be destroyed
* > - Bundle needs to be reloaded to use assets again
* > - Make sure no sprites or other objects are using the assets
* @throws {Error} If the bundle is not found
* @see {@link Assets.addBundle} For adding bundles
* @see {@link Assets.loadBundle} For loading bundles
*/
async unloadBundle(bundleIds) {
if (!this._initialized) {
await this.init();
}
bundleIds = convertToList.convertToList(bundleIds);
const resolveResults = this.resolver.resolveBundle(bundleIds);
const promises = Object.keys(resolveResults).map((bundleId) => this._unloadFromResolved(resolveResults[bundleId]));
await Promise.all(promises);
}
async _unloadFromResolved(resolveResult) {
const resolveArray = Object.values(resolveResult);
resolveArray.forEach((resolveResult2) => {
Cache.Cache.remove(resolveResult2.src);
});
await this.loader.unload(resolveArray);
}
/**
* Detects the supported formats for the browser, and returns an array of supported formats, respecting
* the users preferred formats order.
* @param options - the options to use when detecting formats
* @param options.preferredFormats - the preferred formats to use
* @param options.skipDetections - if we should skip the detections altogether
* @param options.detections - the detections to use
* @returns - the detected formats
*/
async _detectFormats(options) {
let formats = [];
if (options.preferredFormats) {
formats = Array.isArray(options.preferredFormats) ? options.preferredFormats : [options.preferredFormats];
}
for (const detection of options.detections) {
if (options.skipDetections || await detection.test()) {
formats = await detection.add(formats);
} else if (!options.skipDetections) {
formats = await detection.remove(formats);
}
}
formats = formats.filter((format, index) => formats.indexOf(format) === index);
return formats;
}
/**
* All the detection parsers currently added to the Assets class.
* @advanced
*/
get detections() {
return this._detections;
}
/**
* Sets global preferences for asset loading behavior. This method configures how assets
* are loaded and processed across all parsers.
* @param preferences - Asset loading preferences
* @example
* ```ts
* // Basic preferences
* Assets.setPreferences({
* crossOrigin: 'anonymous',
* parseAsGraphicsContext: false
* });
* ```
* @remarks
* Preferences are applied to all compatible parsers and affect future asset loading.
* Common preferences include:
* - `crossOrigin`: CORS setting for loaded assets
* - `preferWorkers`: Whether to use web workers for loading textures
* - `preferCreateImageBitmap`: Use `createImageBitmap` for texture creation. Turning this off will use the `Image` constructor instead.
* @see {@link AssetsPreferences} For all available preferences
*/
setPreferences(preferences) {
this.loader.parsers.forEach((parser) => {
if (!parser.config)
return;
Object.keys(parser.config).filter((key) => key in preferences).forEach((key) => {
parser.config[key] = preferences[key];
});
});
}
}
const Assets = new AssetsClass();
Extensions.extensions.handleByList(Extensions.ExtensionType.LoadParser, Assets.loader.parsers).handleByList(Extensions.ExtensionType.ResolveParser, Assets.resolver.parsers).handleByList(Extensions.ExtensionType.CacheParser, Assets.cache.parsers).handleByList(Extensions.ExtensionType.DetectionParser, Assets.detections);
Extensions.extensions.add(
cacheTextureArray.cacheTextureArray,
detectDefaults.detectDefaults,
detectAvif.detectAvif,
detectWebp.detectWebp,
detectMp4.detectMp4,
detectOgv.detectOgv,
detectWebm.detectWebm,
loadJson.loadJson,
loadTxt.loadTxt,
loadWebFont.loadWebFont,
loadSVG.loadSvg,
loadTextures.loadTextures,
loadVideoTextures.loadVideoTextures,
loadBitmapFont.loadBitmapFont,
loadBitmapFont.bitmapFontCachePlugin,
resolveTextureUrl.resolveTextureUrl,
resolveJsonUrl.resolveJsonUrl
);
const assetKeyMap = {
loader: Extensions.ExtensionType.LoadParser,
resolver: Extensions.ExtensionType.ResolveParser,
cache: Extensions.ExtensionType.CacheParser,
detection: Extensions.ExtensionType.DetectionParser
};
Extensions.extensions.handle(Extensions.ExtensionType.Asset, (extension) => {
const ref = extension.ref;
Object.entries(assetKeyMap).filter(([key]) => !!ref[key]).forEach(([key, type]) => Extensions.extensions.add(Object.assign(
ref[key],
// Allow the function to optionally define it's own
// ExtensionMetadata, the use cases here is priority for LoaderParsers
{ extension: ref[key].extension ?? type }
)));
}, (extension) => {
const ref = extension.ref;
Object.keys(assetKeyMap).filter((key) => !!ref[key]).forEach((key) => Extensions.extensions.remove(ref[key]));
});
exports.Assets = Assets;
exports.AssetsClass = AssetsClass;
//# sourceMappingURL=Assets.js.map