UNPKG

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
'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