Source: texture/texture2d.js

import { ECMASCRIPT_TYPEOF_RESULTS }    from '../constants/ecmascript-types.js';
import { MATH_COMMON_VALUES }           from '../constants/math.js';
import { TEXTURE2D_EXCEPTION_MESSAGES } from '../exception-messages/texture2d.js';
import * as Texture2DConstants          from '../constants/texture2d.js';

/**
 * 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: 0 = none, 1 = always, 2 = 'power-of-two' images only.
 */

/**
 * Texture2D load options, used by 'loadFromUrl'.
 *
 * @typedef {Object} Texture2DLoadOptions
 * @property {("anonymous"|"use-credentials"|null)} [crossOrigin] - Optional cross-origin mode.
 */

/**
 * Resolved sampler settings, used internally before updating the texture.
 *
 * @typedef {Object} Texture2DSamplerParams
 * @property {number} wrapS                 - Horizontal wrap mode.
 * @property {number} wrapT                 - Vertical wrap mode.
 * @property {number} minFilter             - Minification filter.
 * @property {number} magFilter             - Magnification filter.
 * @property {number} mipmapPolicy          - Mipmap generation policy.
 * @property {boolean} hasExplicitMinFilter - Whether the min filter overrides automatic selection.
 * @private
 */

/**
 * '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 = Texture2DConstants.TEXTURE2D_PLACEHOLDER.WIDTH;

    /**
     * Current texture height in pixels. Initialized to placeholder size and updated after a successful upload.
     *
     * @type {number}
     * @private
     */
    #height = Texture2DConstants.TEXTURE2D_PLACEHOLDER.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(TEXTURE2D_EXCEPTION_MESSAGES.EXPECTS_WEBGL2_CONTEXT);
        }

        Texture2D.#assertOptionsObject(options, TEXTURE2D_EXCEPTION_MESSAGES.EXPECTS_OPTIONS_OBJECT);
        this.#webglContext = webglContext;
        this.#initializeOptions(options);
        this.#createTexture();
    }

    /**
     * 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 < Texture2DConstants.TEXTURE2D_LIMITS.MIN_TEXTURE_UNIT_INDEX) {
            throw new TypeError(TEXTURE2D_EXCEPTION_MESSAGES.EXPECTS_TEXTURE_UNIT_INDEX);
        }

        const webglContext = this.#webglContext;
        const maxUnits     = webglContext.getParameter(webglContext.MAX_COMBINED_TEXTURE_IMAGE_UNITS);

        if (!Number.isInteger(maxUnits) || maxUnits <= Texture2DConstants.TEXTURE2D_LIMITS.MIN_TEXTURE_UNIT_INDEX) {
            throw new Error(TEXTURE2D_EXCEPTION_MESSAGES.FAILED_READ_MAX_TEXTURE_UNITS);
        }

        if (textureUnitIndex >= maxUnits) {
            throw new RangeError(TEXTURE2D_EXCEPTION_MESSAGES.TEXTURE_UNIT_INDEX_OUT_OF_RANGE(maxUnits - MATH_COMMON_VALUES.UNIT));
        }

        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();
        Texture2D.#assertOptionsObject(options, TEXTURE2D_EXCEPTION_MESSAGES.EXPECTS_SAMPLER_OPTIONS_OBJECT);

        const hasMipmapPolicy = Object.prototype.hasOwnProperty.call(options, 'mipmapPolicy');
        const mipmapPolicy    = hasMipmapPolicy ? options.mipmapPolicy : this.#mipmapPolicy;
        const sampler         = this.#resolveSamplerParams(options, mipmapPolicy);

        this.#assertSamplerParams(sampler);
        this.#assignSamplerParams(sampler);
        this.#bindTexture();

        try {
            if (this.#isLoaded) {
                const mipmapsGenerated = this.#maybeGenerateMipmaps();
                this.#syncMinFilterWithMipmaps(mipmapsGenerated);
            }

            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 !== ECMASCRIPT_TYPEOF_RESULTS.STRING || url.length < Texture2DConstants.TEXTURE2D_LIMITS.MIN_URL_LENGTH) {
            throw new TypeError(TEXTURE2D_EXCEPTION_MESSAGES.EXPECTS_URL_STRING);
        }

        Texture2D.#assertOptionsObject(options, TEXTURE2D_EXCEPTION_MESSAGES.EXPECTS_LOAD_OPTIONS_OBJECT);
        const crossOrigin = Texture2D.#resolveCrossOrigin(options);
        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;
    }

    /**
     * Checks the options container before reading individual properties.
     *
     * @param {Object} options - Options to validate.
     * @param {string} message - Error message for the calling method.
     * @private
     */
    static #assertOptionsObject(options, message) {
        if (options === null || typeof options !== ECMASCRIPT_TYPEOF_RESULTS.OBJECT || Array.isArray(options)) {
            throw new TypeError(message);
        }
    }

    /**
     * Resolves creation defaults before allocating the WebGL texture.
     *
     * @param {Texture2DOptions} options - Texture creation options.
     * @private
     */
    #initializeOptions(options) {
        const {
            flipY        = Texture2DConstants.TEXTURE2D_DEFAULTS.FLIP_Y,
            mipmapPolicy = Texture2DConstants.TEXTURE2D_DEFAULTS.MIPMAP_POLICY
        } = options;

        if (typeof flipY !== ECMASCRIPT_TYPEOF_RESULTS.BOOLEAN) {
            throw new TypeError(TEXTURE2D_EXCEPTION_MESSAGES.EXPECTS_FLIPY_BOOLEAN);
        }

        if (!this.#isValidMipmapPolicy(mipmapPolicy)) {
            throw new TypeError(TEXTURE2D_EXCEPTION_MESSAGES.EXPECTS_MIPMAP_POLICY);
        }

        this.#flipY     = flipY;
        this.#wrapS     = this.#webglContext.REPEAT;
        this.#wrapT     = this.#webglContext.REPEAT;
        this.#minFilter = this.#webglContext.LINEAR;
        this.#magFilter = this.#webglContext.LINEAR;

        const sampler = this.#resolveSamplerParams(options, mipmapPolicy);
        this.#assertSamplerParams(sampler);
        this.#assignSamplerParams(sampler);
    }

    /**
     * Combines explicitly supplied sampler values with the current settings.
     * Inherited sampler properties are ignored.
     * Setting 'minFilter' to 'null' restores automatic filter selection.
     *
     * @param {Texture2DOptions} options - Sampler overrides.
     * @param {number} mipmapPolicy      - Resolved mipmap policy.
     * @returns {Texture2DSamplerParams} - Sampler values and the explicit min filter flag.
     * @private
     */
    #resolveSamplerParams(options, mipmapPolicy) {
        const hasWrapS     = Object.prototype.hasOwnProperty.call(options, 'wrapS');
        const hasWrapT     = Object.prototype.hasOwnProperty.call(options, 'wrapT');
        const hasMagFilter = Object.prototype.hasOwnProperty.call(options, 'magFilter');
        const minFilter    = this.#resolveMinFilter(options);

        return {
            wrapS     : hasWrapS ? options.wrapS : this.#wrapS,
            wrapT     : hasWrapT ? options.wrapT : this.#wrapT,
            minFilter : minFilter.value,
            magFilter : hasMagFilter ? options.magFilter : this.#magFilter,
            mipmapPolicy,
            hasExplicitMinFilter : minFilter.isExplicit
        };
    }

    /**
     * Distinguishes an omitted min filter, an explicit override and a null reset.
     *
     * @param {Texture2DOptions} options               - Sampler overrides.
     * @returns {{value: number, isExplicit: boolean}} - Min filter value and its explicit override flag.
     * @private
     */
    #resolveMinFilter(options) {
        if (!Object.prototype.hasOwnProperty.call(options, 'minFilter')) {
            return { value: this.#minFilter, isExplicit: this.#hasExplicitMinFilter };
        }

        if (options.minFilter === null) {
            return { value: this.#webglContext.LINEAR, isExplicit: false };
        }

        return { value: options.minFilter, isExplicit: true };
    }

    /**
     * Validates all resolved sampler values before changing the current settings.
     *
     * @param {Texture2DSamplerParams} sampler - Resolved sampler values.
     * @private
     */
    #assertSamplerParams(sampler) {
        if (!this.#isValidWrapMode(sampler.wrapS)) {
            throw new TypeError(TEXTURE2D_EXCEPTION_MESSAGES.EXPECTS_WRAP_S_ENUM);
        }

        if (!this.#isValidWrapMode(sampler.wrapT)) {
            throw new TypeError(TEXTURE2D_EXCEPTION_MESSAGES.EXPECTS_WRAP_T_ENUM);
        }

        if (!this.#isValidMinFilter(sampler.minFilter)) {
            throw new TypeError(TEXTURE2D_EXCEPTION_MESSAGES.EXPECTS_MIN_FILTER_ENUM);
        }

        if (!this.#isValidMagFilter(sampler.magFilter)) {
            throw new TypeError(TEXTURE2D_EXCEPTION_MESSAGES.EXPECTS_MAG_FILTER_ENUM);
        }

        if (!this.#isValidMipmapPolicy(sampler.mipmapPolicy)) {
            throw new TypeError(TEXTURE2D_EXCEPTION_MESSAGES.EXPECTS_MIPMAP_POLICY);
        }

        if (sampler.mipmapPolicy === Texture2DConstants.TEXTURE2D_MIPMAP_POLICIES.NONE
            && sampler.hasExplicitMinFilter
            && this.#isMipmapMinFilter(sampler.minFilter)) {
            throw new TypeError(TEXTURE2D_EXCEPTION_MESSAGES.MIPMAP_POLICY_CONFLICT);
        }
    }

    /**
     * Stores validated sampler settings together.
     *
     * @param {Texture2DSamplerParams} sampler - Validated sampler values.
     * @private
     */
    #assignSamplerParams(sampler) {
        this.#wrapS                = sampler.wrapS;
        this.#wrapT                = sampler.wrapT;
        this.#minFilter            = sampler.minFilter;
        this.#magFilter            = sampler.magFilter;
        this.#mipmapPolicy         = sampler.mipmapPolicy;
        this.#hasExplicitMinFilter = sampler.hasExplicitMinFilter;
    }

    /**
     * Creates the placeholder texture and releases it, if initialization fails.
     *
     * @private
     */
    #createTexture() {
        const texture = this.#webglContext.createTexture();

        if (!texture) {
            throw new Error(TEXTURE2D_EXCEPTION_MESSAGES.FAILED_CREATE_TEXTURE);
        }

        this.#texture = texture;

        try {
            this.#bindTexture();
            this.#uploadPlaceholder();
            this.#applySamplerParams();
        } catch (error) {
            this.#webglContext.deleteTexture(texture);
            throw error;
        } finally {
            this.#unbindTexture();
        }
    }

    /**
     * Uploads the placeholder pixel before a URL image is available.
     *
     * @private
     */
    #uploadPlaceholder() {
        const context     = this.#webglContext;
        const upload      = Texture2DConstants.TEXTURE2D_UPLOAD;
        const placeholder = Texture2DConstants.TEXTURE2D_PLACEHOLDER;

        context.texImage2D(
            context.TEXTURE_2D,
            upload.BASE_MIPMAP_LEVEL,
            context.RGBA,
            placeholder.WIDTH,
            placeholder.HEIGHT,
            upload.BORDER,
            context.RGBA,
            context.UNSIGNED_BYTE,
            Texture2DConstants.TEXTURE2D_PLACEHOLDER_PIXEL
        );
    }

    /**
     * Resolves and validates the optional cross-origin mode.
     *
     * @param {Texture2DLoadOptions} options - Image request options.
     * @returns {string | null}              - Cross-origin mode for the image request.
     * @private
     */
    static #resolveCrossOrigin(options) {
        const hasCrossOrigin = Object.prototype.hasOwnProperty.call(options, 'crossOrigin');
        const crossOrigin    = hasCrossOrigin ? options.crossOrigin : null;

        if (crossOrigin !== null
            && crossOrigin !== Texture2DConstants.TEXTURE2D_CROSS_ORIGIN.ANONYMOUS
            && crossOrigin !== Texture2DConstants.TEXTURE2D_CROSS_ORIGIN.USE_CREDENTIALS) {
            throw new TypeError(TEXTURE2D_EXCEPTION_MESSAGES.EXPECTS_CROSS_ORIGIN);
        }

        return crossOrigin;
    }

    /**
     * Loads an 'HTMLImageElement' from a URL.
     *
     * @param {string} url                  - Image URL.
     * @param {(string|null)} crossOrigin   - 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) {
        return new Promise((resolve, reject) => {
            const image = new Image();

            if (typeof crossOrigin === ECMASCRIPT_TYPEOF_RESULTS.STRING) {
                image.crossOrigin = crossOrigin;
            }

            image.onload  = () => resolve(image);
            image.onerror = () => reject(new Error(TEXTURE2D_EXCEPTION_MESSAGES.FAILED_LOAD_IMAGE(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 {
            this.#setFlipY(this.#flipY);

            webglContext.texImage2D(
                webglContext.TEXTURE_2D,
                Texture2DConstants.TEXTURE2D_UPLOAD.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 {
            this.#setFlipY(previousFlipY);
            this.#unbindTexture();
        }
    }

    /**
     * Applies the upload orientation using the integer values expected by WebGL.
     *
     * @param {boolean} flipY - Vertical image orientation.
     * @private
     */
    #setFlipY(flipY) {
        const webglContext = this.#webglContext;
        const upload       = Texture2DConstants.TEXTURE2D_UPLOAD;
        const value        = flipY ? upload.TRUE_AS_INTEGER : upload.FALSE_AS_INTEGER;
        webglContext.pixelStorei(webglContext.UNPACK_FLIP_Y_WEBGL, value);
    }

    /**
     * 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 === Texture2DConstants.TEXTURE2D_MIPMAP_POLICIES.NONE) {
            return false;
        }

        if (this.#mipmapPolicy === Texture2DConstants.TEXTURE2D_MIPMAP_POLICIES.AUTO
            && !(this.#isPowerOfTwo(this.#width)
            && this.#isPowerOfTwo(this.#height))) {

            if (this.#hasExplicitMinFilter && this.#isMipmapMinFilter(this.#minFilter)) {
                throw new TypeError(TEXTURE2D_EXCEPTION_MESSAGES.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 (this.#hasExplicitMinFilter) {
            return;
        }

        this.#minFilter = mipmapsGenerated
            ? this.#webglContext.LINEAR_MIPMAP_LINEAR
            : 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 === Texture2DConstants.TEXTURE2D_MIPMAP_POLICIES.NONE
            || value === Texture2DConstants.TEXTURE2D_MIPMAP_POLICIES.ALWAYS
            || value === Texture2DConstants.TEXTURE2D_MIPMAP_POLICIES.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 >= MATH_COMMON_VALUES.UNIT
            && (value & (value - MATH_COMMON_VALUES.UNIT)) === MATH_COMMON_VALUES.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(TEXTURE2D_EXCEPTION_MESSAGES.INSTANCE_DISPOSED);
        }
    }
}