/**
* Default value for the `flipY` option.
* When true, uploaded images are flipped vertically during upload via `UNPACK_FLIP_Y_WEBGL`.
*
* @type {boolean}
*/
const DEFAULT_FLIP_Y = true;
/**
* Mipmap policy, never generate the mipmaps.
*
* @type {number}
*/
const MIPMAP_POLICY_NONE = 0;
/**
* Mipmap policy, always generate mipmaps after the successful upload.
*
* @type {number}
*/
const MIPMAP_POLICY_ALWAYS = 1;
/**
* Mipmap policy, generate the mipmaps only, when the texture size is `power-of-two`.
*
* @type {number}
*/
const MIPMAP_POLICY_AUTO = 2;
/**
* Default mipmap generation policy, used by `Texture2D`.
*
* @type {number}
*/
const DEFAULT_MIPMAP_POLICY = MIPMAP_POLICY_AUTO;
/**
* Cross-origin mode, `anonymous`.
*
* @type {string}
*/
const CROSS_ORIGIN_ANONYMOUS = 'anonymous';
/**
* Cross-origin mode, `use-credentials`.
*
* @type {string}
*/
const CROSS_ORIGIN_USE_CREDENTIALS = 'use-credentials';
/**
* Error message: invalid WebGL2 rendering context argument.
*
* @type {string}
*/
const ERROR_EXPECTS_WEBGL2_CONTEXT = '`Texture2D` expects `WebGL2RenderingContext`.';
/**
* Error message: invalid options argument type (must be an object).
*
* @type {string}
*/
const ERROR_EXPECTS_OPTIONS_OBJECT = '`Texture2D` expects options as an object.';
/**
* Error message: invalid `flipY` option type.
*
* @type {string}
*/
const ERROR_EXPECTS_FLIPY_BOOLEAN = '`Texture2D` expects `options.flipY` as boolean.';
/**
* Error message: WebGL failed to create a texture handle.
*
* @type {string}
*/
const ERROR_FAILED_CREATE_TEXTURE = 'Failed to create `WebGLTexture`.';
/**
* Error message: invalid `wrapS` enum value.
*
* @type {string}
*/
const ERROR_EXPECTS_WRAP_S_ENUM = '`Texture2D` expects `options.wrapS` as the valid WebGL wrap mode.';
/**
* Error message: invalid `wrapT` enum value.
*
* @type {string}
*/
const ERROR_EXPECTS_WRAP_T_ENUM = '`Texture2D` expects `options.wrapT` as the valid WebGL wrap mode.';
/**
* Error message: invalid `minFilter` enum value.
*
* @type {string}
*/
const ERROR_EXPECTS_MIN_FILTER_ENUM = '`Texture2D` expects `options.minFilter` as the valid WebGL min filter.';
/**
* Error message: invalid `magFilter` enum value.
*
* @type {string}
*/
const ERROR_EXPECTS_MAG_FILTER_ENUM = '`Texture2D` expects `options.magFilter` as the valid WebGL mag filter.';
/**
* Error message: invalid `mipmapPolicy` enum value.
*
* @type {string}
*/
const ERROR_EXPECTS_MIPMAP_POLICY = '`Texture2D` expects `options.mipmapPolicy` as the valid mipmap policy.';
/**
* Error message: mipmap-minification filter cannot be, used with `MIPMAP_POLICY_NONE`.
*
* @type {string}
*/
const ERROR_MIPMAP_POLICY_CONFLICT = '`Texture2D` cannot use the mipmap min filter, when mipmap policy is NONE.';
/**
* Error message: mipmap min filter was explicitly requested, but mipmaps were not generated by the auto policy.
*
* @type {string}
*/
const ERROR_MIPMAP_AUTO_POT_REQUIRED_FOR_MIPMAP_FILTER = '`Texture2D` cannot apply a mipmap min filter with the auto policy for a `non power-of-two` texture. Use `MIPMAP_POLICY_ALWAYS` or the non-mipmap min filter.';
/**
* Error message: invalid sampler options argument type for `setSamplerParams`.
*
* @type {string}
*/
const ERROR_EXPECTS_SAMPLER_OPTIONS_OBJECT = '`Texture2D.setSamplerParams` expects options as an object.';
/**
* Error message: invalid texture unit index passed to `bind`.
*
* @type {string}
*/
const ERROR_EXPECTS_TEXTURE_UNIT_INDEX = '`Texture2D.bind` expects `textureUnitIndex` as a non-negative integer.';
/**
* Error message prefix: texture unit index is out of range for the current WebGL context.
*
* @type {string}
*/
const ERROR_TEXTURE_UNIT_INDEX_OUT_OF_RANGE_PREFIX = '`Texture2D.bind` texture unit index is out of range. Max allowed index is ';
/**
* Error message: invalid URL argument passed to `loadFromUrl`.
*
* @type {string}
*/
const ERROR_EXPECTS_URL_STRING = '`Texture2D.loadFromUrl` expects url as a non-empty string.';
/**
* Error message: the texture instance is already disposed.
*
* @type {string}
*/
const ERROR_INSTANCE_DISPOSED = '`Texture2D` instance is disposed.';
/**
* Error message: invalid load options argument type for `loadFromUrl`.
*
* @type {string}
*/
const ERROR_EXPECTS_LOAD_OPTIONS_OBJECT = '`Texture2D.loadFromUrl` expects options as an object.';
/**
* Error message: invalid `crossOrigin` value for `loadFromUrl`.
*
* @type {string}
*/
const ERROR_EXPECTS_CROSS_ORIGIN = '`Texture2D.loadFromUrl` expects `options.crossOrigin` as `anonymous`, `use-credentials` or null.';
/**
* Error message prefix used, when the image fails to load.
*
* @type {string}
*/
const ERROR_FAILED_LOAD_IMAGE_PREFIX = 'Failed to load the texture image: ';
/**
* Error message: failed to read `MAX_COMBINED_TEXTURE_IMAGE_UNITS` from WebGL context.
*
* @type {string}
*/
const ERROR_FAILED_READ_MAX_TEXTURE_UNITS = 'Failed to read WebGL `MAX_COMBINED_TEXTURE_IMAGE_UNITS`.';
/**
* Default width of a placeholder texture (in pixels).
*
* @type {number}
*/
const PLACEHOLDER_TEXTURE_WIDTH = 1;
/**
* Default height of a placeholder texture (in pixels).
*
* @type {number}
*/
const PLACEHOLDER_TEXTURE_HEIGHT = 1;
/**
* WebGL expects the border parameter of `texImage2D` to be `0`.
*
* @type {number}
*/
const TEXTURE_BORDER_VALUE = 0;
/**
* Default mipmap level, used by `texImage2D`.
*
* @type {number}
*/
const BASE_MIPMAP_LEVEL = 0;
/**
* Placeholder pixel color (magenta).
*
* @type {Uint8Array}
*/
const PLACEHOLDER_PIXEL_RGBA = new Uint8Array([255, 0, 255, 255]);
/**
* Integer value, used to represent boolean true in WebGL `pixelStorei` calls.
*
* @type {number}
*/
const WEBGL_TRUE_AS_INTEGER = 1;
/**
* Integer value, used to represent boolean false in WebGL `pixelStorei` calls.
*
* @type {number}
*/
const WEBGL_FALSE_AS_INTEGER = 0;
/**
* Minimum allowed texture unit index.
*
* @type {number}
*/
const MIN_TEXTURE_UNIT_INDEX = 0;
/**
* Minimal allowed string length for required input url, used in `loadFromUrl()` method.
*
* @type {number}
*/
const MIN_REQUIRED_STRING_LENGTH = 1;
/**
* Minimal allowed value for `power-of-two` checks.
*
* @type {number}
*/
const MIN_POWER_OF_TWO_VALUE = 1;
/**
* Bit mask helper value, used by the `power-of-two` check.
*
* @type {number}
*/
const BIT_MASK_ONE = 1;
/**
* Bitwise `zero` value, used in bit-mask comparisons.
*
* @type {number}
*/
const BITWISE_ZERO = 0;
/**
* Texture2D creation/update options.
*
* @typedef {Object} Texture2DOptions
* @property {boolean} [flipY = true] - Flip image data vertically on upload.
* @property {number} [wrapS] - WebGL wrap mode for S (REPEAT/CLAMP_TO_EDGE/MIRRORED_REPEAT).
* @property {number} [wrapT] - WebGL wrap mode for T (REPEAT/CLAMP_TO_EDGE/MIRRORED_REPEAT).
* @property {(number|null)} [minFilter] - WebGL min filter (including the mipmap variants). Use `null` to clear explicit override and return to auto behavior.
* @property {number} [magFilter] - WebGL mag filter (NEAREST/LINEAR).
* @property {number} [mipmapPolicy = 2] - `MIPMAP_POLICY_*` constants usage.
*/
/**
* Texture2D load options, used by `loadFromUrl`.
*
* @typedef {Object} Texture2DLoadOptions
* @property {("anonymous"|"use-credentials"|null)} [crossOrigin] - Optional cross-origin mode.
*/
/**
* `Texture2D` is a thin wrapper around a `WebGLTexture`.
* It supports a placeholder pixel (1x1), and can asynchronously upload the image from URL.
*/
export class Texture2D {
/**
* WebGL2 rendering context, used to: create, upload and dispose the underlying WebGL texture.
*
* @type {WebGL2RenderingContext}
* @private
*/
#webglContext;
/**
* Underlying WebGL texture handle.
*
* @type {WebGLTexture}
* @private
*/
#texture;
/**
* When true, uploaded images are flipped vertically during upload. Applied via `UNPACK_FLIP_Y_WEBGL`.
*
* @type {boolean}
* @private
*/
#flipY;
/**
* Current wrap mode for S-coordinate.
*
* @type {number}
* @private
*/
#wrapS;
/**
* Current wrap mode for T-coordinate.
*
* @type {number}
* @private
*/
#wrapT;
/**
* Current minification filter.
*
* @type {number}
* @private
*/
#minFilter;
/**
* Current magnification filter.
*
* @type {number}
* @private
*/
#magFilter;
/**
* Current mipmap generation policy.
*
* @type {number}
* @private
*/
#mipmapPolicy;
/**
* Indicates whether the min filter was explicitly set by the user.
*
* @type {boolean}
* @private
*/
#hasExplicitMinFilter = false;
/**
* Current texture width in pixels. Initialized to placeholder size and updated after a successful upload.
*
* @type {number}
* @private
*/
#width = PLACEHOLDER_TEXTURE_WIDTH;
/**
* Current texture height in pixels. Initialized to placeholder size and updated after a successful upload.
*
* @type {number}
* @private
*/
#height = PLACEHOLDER_TEXTURE_HEIGHT;
/**
* Indicates whether the image has been successfully loaded and uploaded to GPU.
*
* @type {boolean}
* @private
*/
#isLoaded = false;
/**
* Indicates whether this texture instance has been disposed. Disposed textures must not be bound or updated.
*
* @type {boolean}
* @private
*/
#isDisposed = false;
/**
* @param {WebGL2RenderingContext} webglContext - WebGL2 rendering context, used to create and manage the GPU resources.
* @param {Texture2DOptions} [options] - Optional texture creation options.
* @throws {TypeError} When provided arguments do not match expected types or supported enums.
*/
constructor(webglContext, options = {}) {
if (!(webglContext instanceof WebGL2RenderingContext)) {
throw new TypeError(ERROR_EXPECTS_WEBGL2_CONTEXT);
}
if (options === null || typeof options !== 'object' || Array.isArray(options)) {
throw new TypeError(ERROR_EXPECTS_OPTIONS_OBJECT);
}
const {
flipY = DEFAULT_FLIP_Y,
wrapS,
wrapT,
minFilter,
magFilter,
mipmapPolicy = DEFAULT_MIPMAP_POLICY
} = options;
if (typeof flipY !== 'boolean') {
throw new TypeError(ERROR_EXPECTS_FLIPY_BOOLEAN);
}
this.#webglContext = webglContext;
if (!this.#isValidMipmapPolicy(mipmapPolicy)) {
throw new TypeError(ERROR_EXPECTS_MIPMAP_POLICY);
}
const hasMinFilterProperty = Object.prototype.hasOwnProperty.call(options, 'minFilter');
const isResetMinFilter = hasMinFilterProperty && minFilter === null;
const hasExplicitMinFilter = hasMinFilterProperty && !isResetMinFilter;
const hasExplicitWrapS = Object.prototype.hasOwnProperty.call(options, 'wrapS');
const hasExplicitWrapT = Object.prototype.hasOwnProperty.call(options, 'wrapT');
const hasExplicitMagFilter = Object.prototype.hasOwnProperty.call(options, 'magFilter');
const resolvedWrapS = hasExplicitWrapS ? wrapS : webglContext.REPEAT;
const resolvedWrapT = hasExplicitWrapT ? wrapT : webglContext.REPEAT;
const resolvedMinFilter = hasExplicitMinFilter ? minFilter : webglContext.LINEAR;
const resolvedMagFilter = hasExplicitMagFilter ? magFilter : webglContext.LINEAR;
if (hasExplicitWrapS && !this.#isValidWrapMode(resolvedWrapS)) {
throw new TypeError(ERROR_EXPECTS_WRAP_S_ENUM);
}
if (hasExplicitWrapT && !this.#isValidWrapMode(resolvedWrapT)) {
throw new TypeError(ERROR_EXPECTS_WRAP_T_ENUM);
}
if (hasExplicitMinFilter && !this.#isValidMinFilter(resolvedMinFilter)) {
throw new TypeError(ERROR_EXPECTS_MIN_FILTER_ENUM);
}
if (hasExplicitMagFilter && !this.#isValidMagFilter(resolvedMagFilter)) {
throw new TypeError(ERROR_EXPECTS_MAG_FILTER_ENUM);
}
if (mipmapPolicy === MIPMAP_POLICY_NONE
&& hasExplicitMinFilter
&& this.#isMipmapMinFilter(resolvedMinFilter)) {
throw new TypeError(ERROR_MIPMAP_POLICY_CONFLICT);
}
this.#flipY = flipY;
this.#wrapS = resolvedWrapS;
this.#wrapT = resolvedWrapT;
this.#minFilter = resolvedMinFilter;
this.#magFilter = resolvedMagFilter;
this.#mipmapPolicy = mipmapPolicy;
this.#hasExplicitMinFilter = hasExplicitMinFilter;
const texture = webglContext.createTexture();
if (!texture) {
throw new Error(ERROR_FAILED_CREATE_TEXTURE);
}
this.#texture = texture;
this.#bindTexture();
webglContext.texImage2D(
webglContext.TEXTURE_2D,
BASE_MIPMAP_LEVEL,
webglContext.RGBA,
PLACEHOLDER_TEXTURE_WIDTH,
PLACEHOLDER_TEXTURE_HEIGHT,
TEXTURE_BORDER_VALUE,
webglContext.RGBA,
webglContext.UNSIGNED_BYTE,
PLACEHOLDER_PIXEL_RGBA
);
this.#applySamplerParams();
this.#unbindTexture();
}
/**
* Returns the underlying `WebGLTexture` object.
*
* @returns {WebGLTexture}
*/
get texture() {
this.#assertNotDisposed();
return this.#texture;
}
/**
* Returns the width of the uploaded image (or the placeholder width until loaded).
*
* @returns {number}
*/
get width() {
this.#assertNotDisposed();
return this.#width;
}
/**
* Returns the height of the uploaded image (or placeholder height until loaded).
*
* @returns {number}
*/
get height() {
this.#assertNotDisposed();
return this.#height;
}
/**
* Indicates whether the image has been uploaded.
*
* @returns {boolean}
*/
get isLoaded() {
this.#assertNotDisposed();
return this.#isLoaded;
}
/**
* Indicates whether this instance has been disposed.
*
* @returns {boolean}
*/
get isDisposed() {
return this.#isDisposed;
}
/**
* Binds this texture to a texture unit.
*
* @param {number} textureUnitIndex - Index of the texture unit.
*/
bind(textureUnitIndex) {
this.#assertNotDisposed();
if (!Number.isInteger(textureUnitIndex) || textureUnitIndex < MIN_TEXTURE_UNIT_INDEX) {
throw new TypeError(ERROR_EXPECTS_TEXTURE_UNIT_INDEX);
}
const webglContext = this.#webglContext;
const maxUnits = webglContext.getParameter(webglContext.MAX_COMBINED_TEXTURE_IMAGE_UNITS);
if (!Number.isInteger(maxUnits) || maxUnits <= MIN_TEXTURE_UNIT_INDEX) {
throw new Error(ERROR_FAILED_READ_MAX_TEXTURE_UNITS);
}
if (textureUnitIndex >= maxUnits) {
throw new RangeError(`${ERROR_TEXTURE_UNIT_INDEX_OUT_OF_RANGE_PREFIX}${maxUnits - 1}.`);
}
webglContext.activeTexture(webglContext.TEXTURE0 + textureUnitIndex);
webglContext.bindTexture(webglContext.TEXTURE_2D, this.#texture);
}
/**
* Updates sampler parameters for this texture.
*
* @param {Texture2DOptions} [options] - Sampler options to update.
* @throws {TypeError} When provided arguments do not match expected types or supported enums.
*/
setSamplerParams(options = {}) {
this.#assertNotDisposed();
if (options === null || typeof options !== 'object' || Array.isArray(options)) {
throw new TypeError(ERROR_EXPECTS_SAMPLER_OPTIONS_OBJECT);
}
const hasExplicitWrapS = Object.prototype.hasOwnProperty.call(options, 'wrapS');
const hasExplicitWrapT = Object.prototype.hasOwnProperty.call(options, 'wrapT');
const hasMinFilterProperty = Object.prototype.hasOwnProperty.call(options, 'minFilter');
const isResetMinFilter = hasMinFilterProperty && options.minFilter === null;
const hasExplicitMinFilter = hasMinFilterProperty && !isResetMinFilter;
const hasExplicitMagFilter = Object.prototype.hasOwnProperty.call(options, 'magFilter');
const hasExplicitMipmapPolicy = Object.prototype.hasOwnProperty.call(options, 'mipmapPolicy');
const nextWrapS = hasExplicitWrapS ? options.wrapS : this.#wrapS;
const nextWrapT = hasExplicitWrapT ? options.wrapT : this.#wrapT;
const nextMinFilter = isResetMinFilter ? this.#webglContext.LINEAR : (hasExplicitMinFilter ? options.minFilter : this.#minFilter);
const nextMagFilter = hasExplicitMagFilter ? options.magFilter : this.#magFilter;
const nextMipmapPolicy = hasExplicitMipmapPolicy ? options.mipmapPolicy : this.#mipmapPolicy;
const nextHasExplicitMinFilter = isResetMinFilter ? false : (hasExplicitMinFilter ? true : this.#hasExplicitMinFilter);
if (hasExplicitWrapS && !this.#isValidWrapMode(nextWrapS)) {
throw new TypeError(ERROR_EXPECTS_WRAP_S_ENUM);
}
if (hasExplicitWrapT && !this.#isValidWrapMode(nextWrapT)) {
throw new TypeError(ERROR_EXPECTS_WRAP_T_ENUM);
}
if (hasExplicitMinFilter && !this.#isValidMinFilter(nextMinFilter)) {
throw new TypeError(ERROR_EXPECTS_MIN_FILTER_ENUM);
}
if (hasExplicitMagFilter && !this.#isValidMagFilter(nextMagFilter)) {
throw new TypeError(ERROR_EXPECTS_MAG_FILTER_ENUM);
}
if (hasExplicitMipmapPolicy && !this.#isValidMipmapPolicy(nextMipmapPolicy)) {
throw new TypeError(ERROR_EXPECTS_MIPMAP_POLICY);
}
if (nextMipmapPolicy === MIPMAP_POLICY_NONE
&& nextHasExplicitMinFilter
&& this.#isMipmapMinFilter(nextMinFilter)) {
throw new TypeError(ERROR_MIPMAP_POLICY_CONFLICT);
}
this.#wrapS = nextWrapS;
this.#wrapT = nextWrapT;
this.#minFilter = nextMinFilter;
this.#magFilter = nextMagFilter;
this.#mipmapPolicy = nextMipmapPolicy;
this.#hasExplicitMinFilter = nextHasExplicitMinFilter;
this.#bindTexture();
try {
if (this.#isLoaded) {
const mipmapsGenerated = this.#maybeGenerateMipmaps();
this.#syncMinFilterWithMipmaps(mipmapsGenerated);
} else if (!this.#hasExplicitMinFilter && this.#isMipmapMinFilter(this.#minFilter)) {
this.#minFilter = this.#webglContext.LINEAR;
}
this.#applySamplerParams();
} finally {
this.#unbindTexture();
}
}
/**
* Loads an image from the given URL and uploads it into this WebGL texture.
*
* @param {string} url - Image URL (relative or absolute).
* @param {Texture2DLoadOptions} [options] - Optional load options.
* @returns {Promise<void>} - Promise, that resolves after successful GPU upload, or rejects on `load/decode/upload` error.
* @throws {TypeError} When arguments are invalid.
*/
async loadFromUrl(url, options = {}) {
this.#assertNotDisposed();
if (typeof url !== 'string' || url.length < MIN_REQUIRED_STRING_LENGTH) {
throw new TypeError(ERROR_EXPECTS_URL_STRING);
}
if (options === null || typeof options !== 'object' || Array.isArray(options)) {
throw new TypeError(ERROR_EXPECTS_LOAD_OPTIONS_OBJECT);
}
const hasCrossOrigin = Object.prototype.hasOwnProperty.call(options, 'crossOrigin');
const crossOrigin = hasCrossOrigin ? options.crossOrigin : null;
if (hasCrossOrigin
&& crossOrigin !== null
&& crossOrigin !== CROSS_ORIGIN_ANONYMOUS
&& crossOrigin !== CROSS_ORIGIN_USE_CREDENTIALS) {
throw new TypeError(ERROR_EXPECTS_CROSS_ORIGIN);
}
const image = await this.#loadImage(url, crossOrigin);
this.#assertNotDisposed();
this.#uploadImage(image);
}
/**
* Releases the WebGL texture.
*/
dispose() {
if (this.#isDisposed) {
return;
}
this.#webglContext.deleteTexture(this.#texture);
this.#isDisposed = true;
}
/**
* Loads an `HTMLImageElement` from a URL.
*
* @param {string} url - Image URL.
* @param {(string|null)} [crossOrigin = null] - Optional CORS mode: `anonymous/use-credentials`.
* @returns {Promise<HTMLImageElement>} - Promise, that resolves with a decoded image on `load`, or rejects on `error`.
* @private
*/
#loadImage(url, crossOrigin = null) {
return new Promise((resolve, reject) => {
const image = new Image();
if (typeof crossOrigin === 'string') {
image.crossOrigin = crossOrigin;
}
image.onload = () => resolve(image);
image.onerror = () => reject(new Error(`${ERROR_FAILED_LOAD_IMAGE_PREFIX}${url}`));
image.src = url;
});
}
/**
* Uploads the given image into the GPU texture.
*
* @param {HTMLImageElement} image - Loaded image element.
* @private
*/
#uploadImage(image) {
const webglContext = this.#webglContext;
const previousFlipY = webglContext.getParameter(webglContext.UNPACK_FLIP_Y_WEBGL);
this.#bindTexture();
try {
webglContext.pixelStorei(
webglContext.UNPACK_FLIP_Y_WEBGL,
this.#flipY ? WEBGL_TRUE_AS_INTEGER : WEBGL_FALSE_AS_INTEGER
);
webglContext.texImage2D(
webglContext.TEXTURE_2D,
BASE_MIPMAP_LEVEL,
webglContext.RGBA,
webglContext.RGBA,
webglContext.UNSIGNED_BYTE,
image
);
this.#width = image.width;
this.#height = image.height;
this.#isLoaded = true;
const mipmapsGenerated = this.#maybeGenerateMipmaps();
this.#syncMinFilterWithMipmaps(mipmapsGenerated);
this.#applySamplerParams();
} finally {
webglContext.pixelStorei(
webglContext.UNPACK_FLIP_Y_WEBGL,
previousFlipY ? WEBGL_TRUE_AS_INTEGER : WEBGL_FALSE_AS_INTEGER
);
this.#unbindTexture();
}
}
/**
* Applies current sampler parameters to the bound texture.
*
* @private
*/
#applySamplerParams() {
const webglContext = this.#webglContext;
webglContext.texParameteri(webglContext.TEXTURE_2D, webglContext.TEXTURE_WRAP_S, this.#wrapS);
webglContext.texParameteri(webglContext.TEXTURE_2D, webglContext.TEXTURE_WRAP_T, this.#wrapT);
webglContext.texParameteri(webglContext.TEXTURE_2D, webglContext.TEXTURE_MIN_FILTER, this.#minFilter);
webglContext.texParameteri(webglContext.TEXTURE_2D, webglContext.TEXTURE_MAG_FILTER, this.#magFilter);
}
/**
* Generates mipmaps, when the policy allows it.
*
* @returns {boolean} True, when mipmaps were generated.
* @private
*/
#maybeGenerateMipmaps() {
if (this.#mipmapPolicy === MIPMAP_POLICY_NONE) {
return false;
}
if (this.#mipmapPolicy === MIPMAP_POLICY_AUTO
&& !(this.#isPowerOfTwo(this.#width)
&& this.#isPowerOfTwo(this.#height))) {
if (this.#hasExplicitMinFilter && this.#isMipmapMinFilter(this.#minFilter)) {
throw new TypeError(ERROR_MIPMAP_AUTO_POT_REQUIRED_FOR_MIPMAP_FILTER);
}
return false;
}
this.#webglContext.generateMipmap(this.#webglContext.TEXTURE_2D);
return true;
}
/**
* Updates minification filter, based on mipmap availability and explicit overrides.
*
* @param {boolean} mipmapsGenerated - True, when mipmaps were generated.
* @private
*/
#syncMinFilterWithMipmaps(mipmapsGenerated) {
if (mipmapsGenerated) {
if (!this.#hasExplicitMinFilter) {
this.#minFilter = this.#webglContext.LINEAR_MIPMAP_LINEAR;
}
return;
}
if (!this.#hasExplicitMinFilter && this.#isMipmapMinFilter(this.#minFilter)) {
this.#minFilter = this.#webglContext.LINEAR;
}
}
/**
* Checks whether a value is a valid wrap mode.
*
* @param {number} value - Wrap mode value to validate.
* @returns {boolean} True, when value is a supported wrap mode.
* @private
*/
#isValidWrapMode(value) {
const webglContext = this.#webglContext;
return value === webglContext.REPEAT
|| value === webglContext.CLAMP_TO_EDGE
|| value === webglContext.MIRRORED_REPEAT;
}
/**
* Checks whether a value is a valid minification filter.
*
* @param {number} value - Filter value to validate.
* @returns {boolean} True, when value is a supported min filter.
* @private
*/
#isValidMinFilter(value) {
const webglContext = this.#webglContext;
return value === webglContext.NEAREST
|| value === webglContext.LINEAR
|| value === webglContext.NEAREST_MIPMAP_NEAREST
|| value === webglContext.LINEAR_MIPMAP_NEAREST
|| value === webglContext.NEAREST_MIPMAP_LINEAR
|| value === webglContext.LINEAR_MIPMAP_LINEAR;
}
/**
* Checks whether a value is a valid magnification filter.
*
* @param {number} value - Filter value to validate.
* @returns {boolean} True, when value is a supported mag filter.
* @private
*/
#isValidMagFilter(value) {
const webglContext = this.#webglContext;
return value === webglContext.NEAREST
|| value === webglContext.LINEAR;
}
/**
* Checks whether a value is a valid mipmap policy.
*
* @param {number} value - Policy value to validate.
* @returns {boolean} True, when value is a supported policy.
* @private
*/
#isValidMipmapPolicy(value) {
return value === MIPMAP_POLICY_NONE
|| value === MIPMAP_POLICY_ALWAYS
|| value === MIPMAP_POLICY_AUTO;
}
/**
* Checks whether a min filter value uses mipmaps.
*
* @param {number} value - Filter value to validate.
* @returns {boolean} True, when the filter expects mipmaps.
* @private
*/
#isMipmapMinFilter(value) {
const webglContext = this.#webglContext;
return value === webglContext.NEAREST_MIPMAP_NEAREST
|| value === webglContext.LINEAR_MIPMAP_NEAREST
|| value === webglContext.NEAREST_MIPMAP_LINEAR
|| value === webglContext.LINEAR_MIPMAP_LINEAR;
}
/**
* Checks whether an integer value is a `power-of-two`.
*
* @param {number} value - Value to check.
* @returns {boolean} - True if value is a `power-of-two` (e.g.: 2, 4, 8, ...), otherwise false.
* @private
*/
#isPowerOfTwo(value) {
return Number.isInteger(value)
&& value >= MIN_POWER_OF_TWO_VALUE
&& (value & (value - BIT_MASK_ONE)) === BITWISE_ZERO;
}
/**
* @private
*/
#bindTexture() {
this.#webglContext.bindTexture(this.#webglContext.TEXTURE_2D, this.#texture);
}
/**
* @private
*/
#unbindTexture() {
this.#webglContext.bindTexture(this.#webglContext.TEXTURE_2D, null);
}
/**
* @private
*/
#assertNotDisposed() {
if (this.#isDisposed) {
throw new Error(ERROR_INSTANCE_DISPOSED);
}
}
}