UNPKG

shaku

Version:

A simple and effective JavaScript game development framework that knows its place!

1,610 lines (1,386 loc) 633 kB
/******/ (() => { // webpackBootstrap /******/ "use strict"; /******/ var __webpack_modules__ = ({ /***/ 3155: /***/ ((module) => { /** * Assets base class. * * |-- copyright and license --| * @module Shaku * @file shaku\src\assets\asset.js * @author Ronen Ness (ronenness@gmail.com | http://ronenness.com) * @copyright (c) 2021 Ronen Ness * @license MIT * |-- end copyright and license --| * */ /** * A loadable asset base class. * All asset types inherit from this. */ class Asset { /** * Create the new asset. * @param {String} url Asset URL / identifier. */ constructor(url) { this._url = url; this._waitingCallbacks = []; } /** * Get if this asset is ready, ie loaded or created. * @returns {Boolean} True if asset finished loading / creating. This doesn't mean its necessarily valid, only that its done loading. */ get ready() { return this._waitingCallbacks === null; } /** * Register a method to be called when asset is ready. * If asset is already in ready state, will invoke immediately. * @param {Function} callback Callback to invoke when asset is ready. */ onReady(callback) { // already ready if (this.valid || this.ready) { callback(this); return; } // add to callbacks list this._waitingCallbacks.push(callback); } /** * Return a promise to resolve when ready. * @returns {Promise} Promise to resolve when ready. */ waitForReady() { return new Promise((resolve, reject) => { this.onReady(resolve); }); } /** * Notify all waiting callbacks that this asset is ready. * @private */ _notifyReady() { if (this._waitingCallbacks) { for (let i = 0; i < this._waitingCallbacks.length; ++i) { this._waitingCallbacks[i](this); } this._waitingCallbacks = null; } } /** * Get asset's URL. * @returns {String} Asset URL. */ get url() { return this._url; } /** * Get if this asset is loaded and valid. * @returns {Boolean} True if asset is loaded and valid, false otherwise. */ get valid() { throw new Error("Not Implemented!"); } /** * Load the asset from it's URL. * @param {*} params Optional additional params. * @returns {Promise} Promise to resolve when fully loaded. */ load(params) { throw new Error("Not Implemented!"); } /** * Create the asset from data source. * @param {*} source Data to create asset from. * @param {*} params Optional additional params. * @returns {Promise} Promise to resolve when asset is ready. */ create(source, params) { throw new Error("Not Supported for this asset type."); } /** * Destroy the asset, freeing any allocated resources in the process. */ destroy() { throw new Error("Not Implemented!"); } } // export the asset base class. module.exports = Asset; /***/ }), /***/ 7148: /***/ ((module, __unused_webpack_exports, __webpack_require__) => { /** * Implement the assets manager. * * |-- copyright and license --| * @module Shaku * @file shaku\src\assets\assets.js * @author Ronen Ness (ronenness@gmail.com | http://ronenness.com) * @copyright (c) 2021 Ronen Ness * @license MIT * |-- end copyright and license --| * */ const SoundAsset = __webpack_require__(499); const IManager = __webpack_require__(9563); const BinaryAsset = __webpack_require__(3603); const JsonAsset = __webpack_require__(7058); const TextureAsset = __webpack_require__(2262); const FontTextureAsset = __webpack_require__(167); const MsdfFontTextureAsset = __webpack_require__(1252); const Asset = __webpack_require__(3155); const TextureAtlas = __webpack_require__(2493); const TextureAtlasAsset = __webpack_require__(2493); const Vector2 = __webpack_require__(2544); const _logger = (__webpack_require__(5259).getLogger)('assets'); // add a 'isXXX' property to all util objects, for faster alternative to 'instanceof' checks. // for example this will generate a 'isVector3' that will be true for all Vector3 instances. SoundAsset.prototype.isSoundAsset = true; BinaryAsset.prototype.isBinaryAsset = true; JsonAsset.prototype.isJsonAsset = true; TextureAsset.prototype.isTextureAsset = true; FontTextureAsset.prototype.isFontTextureAsset = true; MsdfFontTextureAsset.prototype.isMsdfFontTextureAsset = true; TextureAtlasAsset.prototype.isTextureAtlasAsset = true; /** * Assets manager class. * Used to create, load and cache game assets, which includes textures, audio files, JSON objects, etc. * As a rule of thumb, all methods to load or create assets are async and return a promise. * * To access the Assets manager you use `Shaku.assets`. */ class Assets extends IManager { /** * Create the manager. */ constructor() { super(); this._loaded = null; this._waitingAssets = new Set(); this._failedAssets = new Set(); this._successfulLoadedAssetsCount = 0; /** * Optional URL root to prepend to all loaded assets URLs. * For example, if all your assets are under '/static/assets/', you can set this url as root and omit it when loading assets later. */ this.root = ''; /** * Optional suffix to add to all loaded assets URLs. * You can use this for anti-cache mechanism if you want to reload all assets. For example, you can set this value to "'?dt=' + Date.now()". */ this.suffix = ''; } /** * Wrap a URL with 'root' and 'suffix'. * @param {String} url Url to wrap. * @returns {String} Wrapped URL. */ #_wrapUrl(url) { if (!url) { return url; } return this.root + url + this.suffix; } /** * Get list of assets waiting to be loaded. * This list will be reset if you call clearCache(). * @returns {Array<string>} URLs of assets waiting to be loaded. */ get pendingAssets() { return Array.from(this._waitingAssets); } /** * Get list of assets that failed to load. * This list will be reset if you call clearCache(). * @returns {Array<string>} URLs of assets that had error loading. */ get failedAssets() { return Array.from(this._failedAssets); } /** * Return a promise that will be resolved only when all pending assets are loaded. * If an asset fails, will reject. * @example * await Shaku.assets.waitForAll(); * console.log("All assets are loaded!"); * @returns {Promise} Promise to resolve when all assets are loaded, or reject if there are failed assets. */ waitForAll() { return new Promise((resolve, reject) => { _logger.debug("Waiting for all assets.."); // check if all assets are loaded or if there are errors let checkAssets = () => { // got errors? if (this._failedAssets.size !== 0) { _logger.warn("Done waiting for assets: had errors."); return reject(this.failedAssets); } // all done? if (this._waitingAssets.size === 0) { _logger.debug("Done waiting for assets: everything loaded successfully."); return resolve(); } // try again in 1 ms setTimeout(checkAssets, 1); }; checkAssets(); }); } /** * @inheritdoc * @private */ setup() { return new Promise((resolve, reject) => { _logger.info("Setup assets manager.."); this._loaded = {}; resolve(); }); } /** * @inheritdoc * @private */ startFrame() { } /** * @inheritdoc * @private */ endFrame() { } /** * Get already-loaded asset from cache. * @private * @param {String} url Asset URL. * @param {type} type If provided will make sure asset is of this type. If asset found but have wrong type, will throw exception. * @returns Loaded asset or null if not found. */ #_getFromCache(url, type) { let cached = this._loaded[url] || null; if (cached && type) { if (!(cached instanceof type)) { throw new Error(`Asset with URL '${url}' is already loaded, but has unexpected type (expecting ${type})!`); } } return cached; } /** * Load an asset of a given type and add to cache when done. * @private * @param {Asset} newAsset Asset instance to load. * @param {*} params Optional loading params. */ async #_loadAndCacheAsset(newAsset, params) { // extract url and typename, and add to cache let url = newAsset.url; let typeName = newAsset.constructor.name; this._loaded[url] = newAsset; this._waitingAssets.add(url); // initiate loading return new Promise(async (resolve, reject) => { // load asset _logger.debug(`Load asset [${typeName}] from URL '${url}'.`); try { await newAsset.load(params); } catch (e) { _logger.warn(`Failed to load asset [${typeName}] from URL '${url}'.`); this._failedAssets.add(url); return reject(e); } // update waiting assets count this._waitingAssets.delete(url); // make sure valid if (!newAsset.valid) { _logger.warn(`Failed to load asset [${typeName}] from URL '${url}'.`); this._failedAssets.add(url); return reject("Loaded asset is not valid!"); } _logger.debug(`Successfully loaded asset [${typeName}] from URL '${url}'.`); // resolve this._successfulLoadedAssetsCount++; resolve(newAsset); }); } /** * Get asset directly from cache, synchronous and without a Promise. * @param {String} url Asset URL or name. * @returns {Asset} Asset or null if not loaded. */ getCached(url) { url = this.#_wrapUrl(url); return this._loaded[url] || null; } /** * Get / load asset of given type, and return a promise to be resolved when ready. * @private */ #_loadAssetType(url, typeClass, params) { // normalize URL url = this.#_wrapUrl(url); // try to get from cache let _asset = this.#_getFromCache(url, typeClass); // check if need to create new and load var needLoad = false; if (!_asset) { _asset = new typeClass(url); needLoad = true; } // create promise to load asset let promise = new Promise(async (resolve, reject) => { if (needLoad) { await this.#_loadAndCacheAsset(_asset, params); } _asset.onReady(() => { resolve(_asset); }); }); // return promise with asset attached to it promise.asset = _asset; return promise; } /** * Create and init asset of given class type. * @private */ #_createAsset(name, classType, initMethod, needWait) { // create asset name = this.#_wrapUrl(name); var _asset = new classType(name || generateRandomAssetName()); // if this asset need waiting if (needWait) { this._waitingAssets.add(name); } // generate render target in async let promise = new Promise(async (resolve, reject) => { // make sure not in cache if (name && this._loaded[name]) { return reject(`Asset of type '${classType.name}' to create with URL '${name}' already exist in cache!`); } // create and return await initMethod(_asset); if (name) { this._loaded[name] = _asset; } resolve(_asset); }); // attach asset to promise promise.asset = _asset; return promise; } /** * Load a sound asset. If already loaded, will use cache. * @example * let sound = await Shaku.assets.loadSound("assets/my_sound.ogg"); * @param {String} url Asset URL. * @returns {Promise<SoundAsset>} promise to resolve with asset instance, when loaded. You can access the loading asset with `.asset` on the promise. */ loadSound(url) { return this.#_loadAssetType(url, SoundAsset, undefined); } /** * Load a texture asset. If already loaded, will use cache. * @example * let texture = await Shaku.assets.loadTexture("assets/my_texture.png", {generateMipMaps: false}); * @param {String} url Asset URL. * @param {*=} params Optional params dictionary. See TextureAsset.load() for more details. * @returns {Promise<TextureAsset>} promise to resolve with asset instance, when loaded. You can access the loading asset with `.asset` on the promise. */ loadTexture(url, params) { return this.#_loadAssetType(url, TextureAsset, params); } /** * Create a render target texture asset. If already loaded, will use cache. * @example * let width = 512; * let height = 512; * let renderTarget = await Shaku.assets.createRenderTarget("optional_render_target_asset_id", width, height); * @param {String | null} name Asset name (matched to URLs when using cache). If null, will not add to cache. * @param {Number} width Texture width. * @param {Number} height Texture height. * @param {Number=} channels Texture channels count. Defaults to 4 (RGBA). * @returns {Promise<TextureAsset>} promise to resolve with asset instance, when loaded. You can access the loading asset with `.asset` on the promise. */ createRenderTarget(name, width, height, channels) { // make sure we have valid size if (!width || !height) { throw new Error("Missing or invalid size!"); } // create asset and return promise return this.#_createAsset(name, TextureAsset, (asset) => { asset.createRenderTarget(width, height, channels); }); } /** * Create a texture atlas asset. * @param {String | null} name Asset name (matched to URLs when using cache). If null, will not add to cache. * @param {Array<String>} sources List of URLs to load textures from. * @param {Number=} maxWidth Optional atlas textures max width. * @param {Number=} maxHeight Optional atlas textures max height. * @param {Vector2=} extraMargins Optional extra empty pixels to add between textures in atlas. * @returns {Promise<TextureAtlas>} Promise to resolve with asset instance, when loaded. You can access the loading asset with `.asset` on the promise. */ createTextureAtlas(name, sources, maxWidth, maxHeight, extraMargins) { // make sure we have valid size if (!sources || !sources.length) { throw new Error("Missing or invalid sources!"); } // create asset and return promise return this.#_createAsset(name, TextureAtlasAsset, async (asset) => { try { await asset._build(sources, maxWidth, maxHeight, extraMargins); this._waitingAssets.delete(name); this._successfulLoadedAssetsCount++; } catch (e) { _logger.warn(`Failed to create texture atlas: '${e}'.`); this._failedAssets.add(url); } }, true); } /** * Load a font texture asset. If already loaded, will use cache. * @example * let fontTexture = await Shaku.assets.loadFontTexture('assets/DejaVuSansMono.ttf', {fontName: 'DejaVuSansMono'}); * @param {String} url Asset URL. * @param {*} params Optional params dictionary. See FontTextureAsset.load() for more details. * @returns {Promise<FontTextureAsset>} promise to resolve with asset instance, when loaded. You can access the loading asset with `.asset` on the promise. */ loadFontTexture(url, params) { return this.#_loadAssetType(url, FontTextureAsset, params); } /** * Load a MSDF font texture asset. If already loaded, will use cache. * @example * let fontTexture = await Shaku.assets.loadMsdfFontTexture('DejaVuSansMono.font', {jsonUrl: 'assets/DejaVuSansMono.json', textureUrl: 'assets/DejaVuSansMono.png'}); * @param {String} url Asset URL. * @param {*=} params Optional params dictionary. See MsdfFontTextureAsset.load() for more details. * @returns {Promise<MsdfFontTextureAsset>} promise to resolve with asset instance, when loaded. You can access the loading asset with `.asset` on the promise. */ loadMsdfFontTexture(url, params) { return this.#_loadAssetType(url, MsdfFontTextureAsset, params); } /** * Load a json asset. If already loaded, will use cache. * @example * let jsonData = await Shaku.assets.loadJson('assets/my_json_data.json'); * console.log(jsonData.data); * @param {String} url Asset URL. * @returns {Promise<JsonAsset>} promise to resolve with asset instance, when loaded. You can access the loading asset with `.asset` on the promise. */ loadJson(url) { return this.#_loadAssetType(url, JsonAsset); } /** * Create a new json asset. If already exist, will reject promise. * @example * let jsonData = await Shaku.assets.createJson('optional_json_data_id', {"foo": "bar"}); * // you can now load this asset from anywhere in your code using 'optional_json_data_id' as url * @param {String} name Asset name (matched to URLs when using cache). If null, will not add to cache. * @param {Object|String} data Optional starting data. * @returns {Promise<JsonAsset>} promise to resolve with asset instance, when ready. You can access the loading asset with `.asset` on the promise. */ createJson(name, data) { // make sure we have valid data if (!data) { return reject("Missing or invalid data!"); } // create asset and return promise return this.#_createAsset(name, JsonAsset, (asset) => { asset.create(data); }); } /** * Load a binary data asset. If already loaded, will use cache. * @example * let binData = await Shaku.assets.loadBinary('assets/my_bin_data.dat'); * console.log(binData.data); * @param {String} url Asset URL. * @returns {Promise<BinaryAsset>} promise to resolve with asset instance, when loaded. You can access the loading asset with `.asset` on the promise. */ loadBinary(url) { return this.#_loadAssetType(url, BinaryAsset); } /** * Create a new binary asset. If already exist, will reject promise. * @example * let binData = await Shaku.assets.createBinary('optional_bin_data_id', [1,2,3,4]); * // you can now load this asset from anywhere in your code using 'optional_bin_data_id' as url * @param {String} name Asset name (matched to URLs when using cache). If null, will not add to cache. * @param {Array<Number>|Uint8Array} data Binary data to set. * @returns {Promise<BinaryAsset>} promise to resolve with asset instance, when ready. You can access the loading asset with `.asset` on the promise. */ createBinary(name, data) { // make sure we have valid data if (!data) { return reject("Missing or invalid data!"); } // create asset and return promise return this.#_createAsset(name, BinaryAsset, (asset) => { asset.create(data); }); } /** * Destroy and free asset from cache. * @example * Shaku.assets.free("my_asset_url"); * @param {String} url Asset URL to free. */ free(url) { url = this.#_wrapUrl(url); let asset = this._loaded[url]; if (asset) { asset.destroy(); delete this._loaded[url]; } } /** * Free all loaded assets from cache. * @example * Shaku.assets.clearCache(); */ clearCache() { for (let key in this._loaded) { this._loaded[key].destroy(); } this._loaded = {}; this._waitingAssets = new Set(); this._failedAssets = new Set(); } /** * @inheritdoc * @private */ destroy() { this.clearCache(); } } // generate a random asset URL, for when creating assets that are outside of cache. var _nextRandomAssetId = 0; function generateRandomAssetName() { return "_runtime_asset_" + (_nextRandomAssetId++) + "_"; } // export assets manager module.exports = new Assets(); /***/ }), /***/ 3603: /***/ ((module, __unused_webpack_exports, __webpack_require__) => { /** * Implement binary data asset type. * * |-- copyright and license --| * @module Shaku * @file shaku\src\assets\binary_asset.js * @author Ronen Ness (ronenness@gmail.com | http://ronenness.com) * @copyright (c) 2021 Ronen Ness * @license MIT * |-- end copyright and license --| * */ const Asset = __webpack_require__(3155); /** * A loadable binary data asset. * This asset type loads array of bytes from a remote file. */ class BinaryAsset extends Asset { /** @inheritdoc */ constructor(url) { super(url); this._data = null; } /** * Load the binary data from the asset URL. * @returns {Promise} Promise to resolve when fully loaded. */ load() { return new Promise((resolve, reject) => { var request = new XMLHttpRequest(); request.open('GET', this.url, true); request.responseType = 'arraybuffer'; // on load, validate audio content request.onload = () => { if (request.readyState == 4) { if (request.response) { this._data = new Uint8Array(request.response); this._notifyReady(); resolve(); } else { reject(request.statusText); } } } // on load error, reject request.onerror = (e) => { reject(e); } // initiate request request.send(); }); } /** * Create the binary data asset from array or Uint8Array. * @param {Array<Number>|Uint8Array} source Data to create asset from. * @returns {Promise} Promise to resolve when asset is ready. */ create(source) { return new Promise((resolve, reject) => { if (Array.isArray(source)) { source = new Uint8Array(source); } if (!(source instanceof Uint8Array)) { return reject("Binary asset source must be of type 'Uint8Array'!"); } this._data = source; this._notifyReady(); resolve(); }); } /** @inheritdoc */ get valid() { return Boolean(this._data); } /** @inheritdoc */ destroy() { this._data = null; } /** * Get binary data. * @returns {Uint8Array} Data as bytes array. */ get data() { return this._data; } /** * Convert and return data as string. * @returns {String} Data converted to string. */ string() { return (new TextDecoder()).decode(this._data); } } // export the asset type. module.exports = BinaryAsset; /***/ }), /***/ 167: /***/ ((module, __unused_webpack_exports, __webpack_require__) => { /** * Implement a font texture asset type. * * |-- copyright and license --| * @module Shaku * @file shaku\src\assets\font_texture_asset.js * @author Ronen Ness (ronenness@gmail.com | http://ronenness.com) * @copyright (c) 2021 Ronen Ness * @license MIT * |-- end copyright and license --| * */ const Asset = __webpack_require__(3155); const Vector2 = __webpack_require__(2544); const Rectangle = __webpack_require__(4731); const TextureAsset = __webpack_require__(2262); /** * A font texture asset, dynamically generated from loaded font and canvas. * This asset type creates an atlas of all the font's characters as textures, so we can later render them as sprites. */ class FontTextureAsset extends Asset { /** @inheritdoc */ constructor(url) { super(url); this._fontName = null; this._fontSize = null; this._placeholderChar = null; this._sourceRects = null; this._texture = null; this._lineHeight = 0; } /** * Get line height. */ get lineHeight() { return this._lineHeight; } /** * Get font name. */ get fontName() { return this._fontName; } /** * Get font size. */ get fontSize() { return this._fontSize; } /** * Get placeholder character. */ get placeholderCharacter() { return this._placeholderChar; } /** * Get the texture. */ get texture() { return this._texture; } /** * Generate the font texture from a font found in given URL. * @param {*} params Additional params. Possible values are: * - fontName: mandatory font name. on some browsers if the font name does not match the font you actually load via the URL, it will not be loaded properly. * - missingCharPlaceholder (default='?'): character to use for missing characters. * - smoothFont (default=true): if true, will set font to smooth mode. * - fontSize (default=52): font size in texture. larget font size will take more memory, but allow for sharper text rendering in larger scales. * - enforceTexturePowerOfTwo (default=true): if true, will force texture size to be power of two. * - maxTextureWidth (default=1024): max texture width. * - charactersSet (default=FontTextureAsset.defaultCharactersSet): which characters to set in the texture. * - extraPadding (default=0,0): Optional extra padding to add around characters in texture. * - sourceRectOffsetAdjustment (default=0,0): Optional extra offset in characters source rectangles. Use this for fonts that are too low / height and bleed into other characters source rectangles. * @returns {Promise} Promise to resolve when fully loaded. */ load(params) { return new Promise(async (resolve, reject) => { if (!params || !params.fontName) { return reject("When loading font texture you must provide params with a 'fontName' value!"); } // set default missing char placeholder + store it this._placeholderChar = (params.missingCharPlaceholder || '?')[0]; // set smoothing mode let smooth = params.smoothFont === undefined ? true : params.smoothFont; // set extra margins let extraPadding = params.extraPadding || {x: 0, y: 0}; // set max texture size let maxTextureWidth = params.maxTextureWidth || 1024; // default chars set let charsSet = params.charactersSet || FontTextureAsset.defaultCharactersSet; // make sure charSet got the placeholder char if (charsSet.indexOf(this._placeholderChar) === -1) { charsSet += this._placeholderChar; } // load font let fontFace = new FontFace(params.fontName, `url(${this.url})`); await fontFace.load(); document.fonts.add(fontFace); // store font name and size this._fontName = params.fontName; this._fontSize = params.fontSize || 52; let margin = {x: 10, y: 5}; // measure font height let fontFullName = this.fontSize.toString() + 'px ' + this.fontName; let fontHeight = measureTextHeight(this.fontName, this.fontSize, undefined, extraPadding.y); let fontWidth = measureTextWidth(this.fontName, this.fontSize, undefined, extraPadding.x); // set line height this._lineHeight = fontHeight; // calc estimated size of a single character in texture let estimatedCharSizeInTexture = new Vector2(fontWidth + margin.x * 2, fontHeight + margin.y * 2); // calc texture size let charsPerRow = Math.floor(maxTextureWidth / estimatedCharSizeInTexture.x); let textureWidth = Math.min(charsSet.length * estimatedCharSizeInTexture.x, maxTextureWidth); let textureHeight = Math.ceil(charsSet.length / charsPerRow) * (estimatedCharSizeInTexture.y); // make width and height powers of two if (params.enforceTexturePowerOfTwo || params.enforceTexturePowerOfTwo === undefined) { textureWidth = makePowerTwo(textureWidth); textureHeight = makePowerTwo(textureHeight); } // a dictionary to store the source rect of every character this._sourceRects = {}; // create a canvas to generate the texture on let canvas = document.createElement('canvas'); canvas.width = textureWidth; canvas.height = textureHeight; if (!smooth) { canvas.style.webkitFontSmoothing = "none"; canvas.style.fontSmooth = "never"; canvas.style.textRendering = "geometricPrecision"; } let ctx = canvas.getContext('2d'); ctx.textBaseline = "bottom" // set font and white color ctx.font = fontFullName; ctx.fillStyle = '#ffffffff'; ctx.imageSmoothingEnabled = smooth; // draw the font texture let x = 0; let y = 0; for (let i = 0; i < charsSet.length; ++i) { // get actual width of current character let currChar = charsSet[i]; let currCharWidth = Math.ceil(ctx.measureText(currChar).width + extraPadding.x); // check if need to break line down in texture if (x + currCharWidth > textureWidth) { y += Math.round(fontHeight + margin.y); x = 0; } // calc source rect const offsetAdjustment = params.sourceRectOffsetAdjustment || {x: 0, y: 0}; let sourceRect = new Rectangle(x + offsetAdjustment.x, y + offsetAdjustment.y, currCharWidth, fontHeight); this._sourceRects[currChar] = sourceRect; // draw character ctx.fillText(currChar, x, y + fontHeight); // move to next spot in texture x += Math.round(currCharWidth + margin.x); } // do threshold effect if (!smooth) { let imageData = ctx.getImageData(0, 0, ctx.canvas.width, ctx.canvas.height); let data = imageData.data; for (let i = 0; i < data.length; i += 4) { if (data[i+3] > 0 && (data[i+3] < 255 || data[i] < 255 || data[i+1] < 255 || data[i+2] < 255)) { data[i + 3] = 0; } } ctx.putImageData(imageData, 0, 0); } // convert canvas to image let img = new Image(); img.src = canvas.toDataURL("image/png"); img.onload = () => { // convert image to texture let texture = new TextureAsset(this.url + '__font-texture'); texture.fromImage(img); // success! this._texture = texture; this._notifyReady(); resolve(); }; }); } /** * Get texture width. * @returns {Number} Texture width. */ get width() { return this._texture._width; } /** * Get texture height. * @returns {Number} Texture height. */ get height() { return this._texture._height; } /** * Get texture size as a vector. * @returns {Vector2} Texture size. */ getSize() { return this._texture.getSize(); } /** @inheritdoc */ get valid() { return Boolean(this._texture); } /** * Get the source rectangle for a given character in texture. * @param {Character} character Character to get source rect for. * @returns {Rectangle} Source rectangle for character. */ getSourceRect(character) { return this._sourceRects[character] || this._sourceRects[this.placeholderCharacter]; } /** * When drawing the character, get the offset to add to the cursor. * @param {Character} character Character to get the offset for. * @returns {Vector2} Offset to add to the cursor before drawing the character. */ getPositionOffset (character) { return Vector2.zero(); } /** * Get how much to advance the cursor when drawing this character. * @param {Character} character Character to get the advance for. * @returns {Number} Distance to move the cursor after drawing the character. */ getXAdvance (character) { return this.getSourceRect(character).width; } /** @inheritdoc */ destroy() { if (this._texture) this._texture.destroy(); this._fontName = null; this._fontSize = null; this._placeholderChar = null; this._sourceRects = null; this._texture = null; this._lineHeight = 0; } } // default ascii characters to generate font textures for FontTextureAsset.defaultCharactersSet = " !\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|}~ ¡¢£¤¥¦§¨©ª«¬­®¯°±²³´µ¶·¸¹º»¼½¾"; // return the closest power-of-two value to a given number function makePowerTwo(val) { let ret = 2; while (ret < val) { if (ret >= val) { return ret; } ret = ret * 2; } return ret; } /** * Measure font's actual height. */ function measureTextHeight(fontFamily, fontSize, char, extraHeight) { let text = document.createElement('pre'); text.style.fontFamily = fontFamily; text.style.fontSize = fontSize + "px"; text.style.paddingBottom = text.style.paddingLeft = text.style.paddingTop = text.style.paddingRight = '0px'; text.style.marginBottom = text.style.marginLeft = text.style.marginTop = text.style.marginRight = '0px'; text.textContent = char || "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789 "; document.body.appendChild(text); let result = text.getBoundingClientRect().height + (extraHeight || 0); document.body.removeChild(text); return Math.ceil(result); }; /** * Measure font's actual width. */ function measureTextWidth(fontFamily, fontSize, char, extraWidth) { // special case to ignore \r and \n when measuring text width if (char === '\n' || char === '\r') { return 0; } // measure character width let canvas = document.createElement("canvas"); let context = canvas.getContext("2d"); context.font = fontSize.toString() + 'px ' + fontFamily; let result = 0; let text = char || "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789 "; for (let i = 0; i < text.length; ++i) { result = Math.max(result, context.measureText(text[i]).width + (extraWidth || 0)); } return Math.ceil(result); }; // export the asset type. module.exports = FontTextureAsset; /***/ }), /***/ 7817: /***/ ((module, __unused_webpack_exports, __webpack_require__) => { /** * Just an alias to main manager so we can require() this folder as a package. * * |-- copyright and license --| * @module Shaku * @file shaku\src\assets\index.js * @author Ronen Ness (ronenness@gmail.com | http://ronenness.com) * @copyright (c) 2021 Ronen Ness * @license MIT * |-- end copyright and license --| * */ module.exports = __webpack_require__(7148); /***/ }), /***/ 7058: /***/ ((module, __unused_webpack_exports, __webpack_require__) => { /** * Implement json asset type. * * |-- copyright and license --| * @module Shaku * @file shaku\src\assets\json_asset.js * @author Ronen Ness (ronenness@gmail.com | http://ronenness.com) * @copyright (c) 2021 Ronen Ness * @license MIT * |-- end copyright and license --| * */ const Asset = __webpack_require__(3155); /** * A loadable json asset. * This asset type loads JSON from a remote file. */ class JsonAsset extends Asset { /** @inheritdoc */ constructor(url) { super(url); this._data = null; } /** * Load the JSON data from the asset URL. * @returns {Promise} Promise to resolve when fully loaded. */ load() { return new Promise((resolve, reject) => { var request = new XMLHttpRequest(); request.open('GET', this.url, true); request.responseType = 'json'; // on load, validate audio content request.onload = () => { if (request.readyState == 4) { if (request.response) { this._data = request.response; this._notifyReady(); resolve(); } else { if (request.status === 200) { reject("Response is not a valid JSON!"); } else { reject(request.statusText); } } } } // on load error, reject request.onerror = (e) => { reject(e); } // initiate request request.send(); }); } /** * Create the JSON data asset from object or string. * @param {Object|String} source Data to create asset from. * @returns {Promise} Promise to resolve when asset is ready. */ create(source) { return new Promise((resolve, reject) => { // make sure data is a valid json + clone it try { if (source) { if (typeof source === 'string') { source = JSON.parse(source); } else { source = JSON.parse(JSON.stringify(source)); } } else { source = {}; } } catch (e) { return reject("Data is not a valid JSON serializable object!"); } // store data and resolve this._data = source; this._notifyReady(); resolve(); }); } /** * Get json data. * @returns {*} Data as dictionary. */ get data() { return this._data; } /** @inheritdoc */ get valid() { return Boolean(this._data); } /** @inheritdoc */ destroy() { this._data = null; } } // export the asset type. module.exports = JsonAsset; /***/ }), /***/ 1252: /***/ ((module, __unused_webpack_exports, __webpack_require__) => { /** * Implement a MSDF font texture asset type. * * |-- copyright and license --| * @module Shaku * @file shaku\src\assets\msdf_font_texture_asset.js * @author Ronen Ness (ronenness@gmail.com | http://ronenness.com) * @copyright (c) 2021 Ronen Ness * @license MIT * |-- end copyright and license --| * */ const Vector2 = __webpack_require__(2544); const Rectangle = __webpack_require__(4731); const TextureAsset = __webpack_require__(2262); const FontTextureAsset = __webpack_require__(167) const JsonAsset = __webpack_require__(7058) const TextureFilterModes = __webpack_require__(5387) /** * A MSDF font texture asset, from a pregenerated msdf texture atlas (from msdf-bmfont-xml, for example). * This asset uses a signed distance field atlas to render characters as sprites at high res. */ class MsdfFontTextureAsset extends FontTextureAsset { /** @inheritdoc */ constructor(url) { super(url) this._positionOffsets = null this._xAdvances = null } /** * Generate the font metadata and texture from the given URL. * @param {*} params Additional params. Possible values are: * - jsonUrl: mandatory url for the font's json metadata (generated via msdf-bmfont-xml, for example) * - textureUrl: mandatory url for the font's texture atlas (generated via msdf-bmfont-xml, for example) * - missingCharPlaceholder (default='?'): character to use for missing characters. * * @returns {Promise} Promise to resolve when fully loaded. */ load(params) { return new Promise(async (resolve, reject) => { if (!params || !params.jsonUrl || !params.textureUrl) { return reject("When loading an msdf font you must provide params with a 'jsonUrl' and a 'textureUrl'!"); } // TODO: allow atlas with multiple textures // TODO: infer textureUrl from json contents // TODO: infer jsonUrl from url let atlas_json = new JsonAsset(params.jsonUrl); let atlas_texture = new TextureAsset(params.textureUrl); await Promise.all([atlas_json.load(), atlas_texture.load()]); let atlas_metadata = atlas_json.data; atlas_texture.filter = TextureFilterModes.Linear; if (atlas_metadata.common.pages > 1) { throw new Error("Can't use MSDF font with several pages"); } // set default missing char placeholder + store it this._placeholderChar = (params.missingCharPlaceholder || '?')[0]; if (!atlas_metadata.info.charset.includes(this._placeholderChar)) { throw new Error("The atlas' charset doesn't include the given placeholder character"); } this._fontName = atlas_metadata.info.face; this._fontSize = atlas_metadata.info.size; // set line height this._lineHeight = atlas_metadata.common.lineHeight; // dictionaries to store per-character data this._sourceRects = {}; this._positionOffsets = {}; this._xAdvances = {}; this._kernings = {}; for (const charData of atlas_metadata.chars) { let currChar = charData.char; let sourceRect = new Rectangle(charData.x, charData.y, charData.width, charData.height) this._sourceRects[currChar] = sourceRect; this._positionOffsets[currChar] = new Vector2( charData.xoffset, charData.yoffset ) this._xAdvances[currChar] = charData.xadvance } this._texture = atlas_texture; this._notifyReady(); resolve(); }); } /** * Get texture width. * @returns {Number} Texture width. */ get width() { return this._texture._width; } /** * Get texture height. * @returns {Number} Texture height. */ get height() { return this._texture._height; } /** * Get texture size as a vector. * @returns {Vector2} Texture size. */ getSize() { return this._texture.getSize(); } /** @inheritdoc */ getPositionOffset (character) { return this._positionOffsets[character] || this._positionOffsets[this.placeholderCharacter]; } /** @inheritdoc */ getXAdvance (character) { return this._xAdvances[character] || this._xAdvances[this.placeholderCharacter]; } /** @inheritdoc */ destroy() { super.destroy(); this._positionOffsets = null this._xAdvances = null this._kernings = null } } // export the asset type. module.exports = MsdfFontTextureAsset; /***/ }), /***/ 499: /***/ ((module, __unused_webpack_exports, __webpack_require__) => { /** * Implement sound asset type. * * |-- copyright and license --| * @module Shaku * @file shaku\src\assets\sound_asset.js * @author Ronen Ness (ronenness@gmail.com | http://ronenness.com) * @copyright (c) 2021 Ronen Ness * @license MIT * |-- end copyright and license --| * */ const Asset = __webpack_require__(3155); /** * A loadable sound asset. * This is the asset type you use to play sounds. */ class SoundAsset extends Asset { /** @inheritdoc */ constructor(url) { super(url); this._valid = false; } /** * Load the sound asset from its URL. * Note that loading sounds isn't actually necessary to play sounds, this method merely pre-load the asset (so first time we play * the sound would be immediate and not delayed) and validate the data is valid. * @returns {Promise} Promise to resolve when fully loaded. */ load() { // for audio files we force preload and validation of the audio file. // note: we can't use the Audio object as it won't work without page interaction. return new Promise((resolve, reject) => { // create request to load audio file var request = new XMLHttpRequest(); request.open('GET', this.url, true); request.responseType = 'arraybuffer'; // on load, validate audio content request.onload = () => { this._valid = true; this._notifyReady(); resolve(); } // on load error, reject request.onerror = (e) => { reject(e); } // initiate request request.send(); }); } /** @inheritdoc */ get valid() { return this._valid; } /** @inheritdoc */ destroy() { this._valid = false; } } // export the asset type. module.exports = SoundAsset; /***/ }), /***/ 2262: /***/ ((module, __unused_webpack_exports, __webpack_require__) => { /** * Implement texture asset type. * * |-- copyright and license --| * @module Shaku * @file shaku\src\assets\texture_asset.js * @author Ronen Ness (ronenness@gmail.com | http://ronenness.com) * @copyright (c) 2021 Ronen Ness * @license MIT * |-- end copyright and license --| * */ const Color = __webpack_require__(9327); const TextureAssetBase = __webpack_require__(4397); const _logger = (__webpack_require__(5259).getLogger)('assets'); // the webgl context to use var gl = null; /** * A loadable texture asset. * This asset type loads an image from URL or source, and turn it into a texture. */ class TextureAsset extends TextureAssetBase { /** @inheritdoc */ constructor(url) { super(url); this._image = null; this._width = 0; this._height = 0; this._texture = nu