API Docs for:

File: /home/lindsay/xeolabs/xeogl-next/xeogl/src/materials/texture.js

/**
 A **Texture** specifies a texture map.

 ## Overview

 * Textures are grouped within {{#crossLink "Material"}}Materials{{/crossLink}}, which are attached to
 {{#crossLink "Mesh"}}Meshes{{/crossLink}}.
 * To create a Texture from an image file, set the Texture's {{#crossLink "Texture/src:property"}}{{/crossLink}}
 property to the image file path.
 * To create a Texture from an HTMLImageElement, set the Texture's {{#crossLink "Texture/image:property"}}{{/crossLink}}
 property to the HTMLImageElement.

 ## Examples

 * [Textures on MetallicMaterials](../../examples/#materials_metallic_textures)
 * [Textures on SpecularMaterials](../../examples/#materials_specGloss_textures)
 * [Textures on PhongMaterials](../../examples/#materials_phong_textures)
 * [Video texture](../../examples/#materials_phong_textures_video)

 ## Usage

 In this example we have a Mesh with

 * a {{#crossLink "PhongMaterial"}}{{/crossLink}} which applies diffuse and specular {{#crossLink "Texture"}}Textures{{/crossLink}}, and
 * a {{#crossLink "TorusGeometry"}}{{/crossLink}}.

 Note that xeogl will ignore the {{#crossLink "PhongMaterial"}}PhongMaterial's{{/crossLink}} {{#crossLink "PhongMaterial/diffuse:property"}}{{/crossLink}}
 and {{#crossLink "PhongMaterial/specular:property"}}{{/crossLink}} properties, since we assigned {{#crossLink "Texture"}}Textures{{/crossLink}} to the {{#crossLink "PhongMaterial"}}PhongMaterial's{{/crossLink}} {{#crossLink "PhongMaterial/diffuseMap:property"}}{{/crossLink}} and
 {{#crossLink "PhongMaterial/specularMap:property"}}{{/crossLink}} properties. The {{#crossLink "Texture"}}Textures'{{/crossLink}} pixel
 colors directly provide the diffuse and specular components for each fragment across the {{#crossLink "Geometry"}}{{/crossLink}} surface.

 ```` javascript
 var mesh = new xeogl.Mesh({

    material: new xeogl.PhongMaterial({
        ambient: [0.3, 0.3, 0.3],
        diffuse: [0.5, 0.5, 0.0],   // Ignored, since we have assigned a Texture to diffuseMap, below
        specular: [1.0, 1.0, 1.0],   // Ignored, since we have assigned a Texture to specularMap, below
        diffuseMap: new xeogl.Texture({
            src: "diffuseMap.jpg"
        }),
        specularMap: new xeogl.Fresnel({
            src: "diffuseMap.jpg"
        }),
        shininess: 80, // Default
        alpha: 1.0 // Default
    }),

    geometry: new xeogl.TorusGeometry()
});
 ````

 @class Texture
 @module xeogl
 @submodule materials
 @constructor
 @param [owner] {Component} Owner component. When destroyed, the owner will destroy this component as well. Creates this component within the default {{#crossLink "Scene"}}{{/crossLink}} when omitted.
 @param [cfg] {*} Configs
 @param [cfg.id] {String} Optional ID for this Texture, unique among all components in the parent scene, generated automatically when omitted.
 @param [cfg.meta] {String:Object} Optional map of user-defined metadata to attach to this Texture.
 @param [cfg.src=null] {String} Path to image file to load into this Texture. See the {{#crossLink "Texture/src:property"}}{{/crossLink}} property for more info.
 @param [cfg.image=null] {HTMLImageElement} HTML Image object to load into this Texture. See the {{#crossLink "Texture/image:property"}}{{/crossLink}} property for more info.
 @param [cfg.minFilter="linearMipmapLinear"] {String} How the texture is sampled when a texel covers less than one pixel. See the {{#crossLink "Texture/minFilter:property"}}{{/crossLink}} property for more info.
 @param [cfg.magFilter="linear"] {String} How the texture is sampled when a texel covers more than one pixel. See the {{#crossLink "Texture/magFilter:property"}}{{/crossLink}} property for more info.
 @param [cfg.wrapS="repeat"] {String} Wrap parameter for texture coordinate *S*. See the {{#crossLink "Texture/wrapS:property"}}{{/crossLink}} property for more info.
 @param [cfg.wrapT="repeat"] {String} Wrap parameter for texture coordinate *S*. See the {{#crossLink "Texture/wrapT:property"}}{{/crossLink}} property for more info.
 @param [cfg.flipY=false] {Boolean} Flips this Texture's source data along its vertical axis when true.
 @param [cfg.translate=[0,0]] {Array of Number} 2D translation vector that will be added to texture's *S* and *T* coordinates.
 @param [cfg.scale=[1,1]] {Array of Number} 2D scaling vector that will be applied to texture's *S* and *T* coordinates.
 @param [cfg.rotate=0] {Number} Rotation, in degrees, that will be applied to texture's *S* and *T* coordinates.
 @param [cfg.encoding="linear"] {String} Encoding format.  See the {{#crossLink "Texture/encoding:property"}}{{/crossLink}} property for more info.
 @extends Component
 */
import {core} from "./../core.js";
import {Component} from '../component.js';
import {State} from '../renderer/state.js';
import {Texture2D} from '../renderer/texture2d.js';
import {math} from '../math/math.js';
import {stats} from './../stats.js';
import {componentClasses} from "./../componentClasses.js";

const type = "xeogl.Texture";

function ensureImageSizePowerOfTwo(image) {
    if (!isPowerOfTwo(image.width) || !isPowerOfTwo(image.height)) {
        const canvas = document.createElement("canvas");
        canvas.width = nextHighestPowerOfTwo(image.width);
        canvas.height = nextHighestPowerOfTwo(image.height);
        const ctx = canvas.getContext("2d");
        ctx.drawImage(image,
            0, 0, image.width, image.height,
            0, 0, canvas.width, canvas.height);
        image = canvas;
    }
    return image;
}

function isPowerOfTwo(x) {
    return (x & (x - 1)) === 0;
}

function nextHighestPowerOfTwo(x) {
    --x;
    for (let i = 1; i < 32; i <<= 1) {
        x = x | x >> i;
    }
    return x + 1;
}

class Texture extends Component {

    /**
     JavaScript class name for this Component.

     For example: "xeogl.AmbientLight", "xeogl.MetallicMaterial" etc.

     @property type
     @type String
     @final
     */
    get type() {
        return type;
    }

    init(cfg) {

        super.init(cfg);

        this._state = new State({
            texture: new Texture2D(this.scene.canvas.gl),
            matrix: math.identityMat4(),   // Float32Array
            hasMatrix: (cfg.translate && (cfg.translate[0] !== 0 || cfg.translate[1] !== 0)) || (!!cfg.rotate) || (cfg.scale && (cfg.scale[0] !== 0 || cfg.scale[1] !== 0)),
            minFilter: this._checkMinFilter(cfg.minFilter),
            magFilter: this._checkMagFilter(cfg.minFilter),
            wrapS: this._checkWrapS(cfg.wrapS),
            wrapT: this._checkWrapT(cfg.wrapT),
            flipY: this._checkFlipY(cfg.flipY),
            encoding: this._checkEncoding(cfg.encoding)
        });

        // Data source

        this._src = null;
        this._image = null;

        // Transformation

        this._translate = math.vec2([0, 0]);
        this._scale = math.vec2([1, 1]);
        this._rotate = math.vec2([0, 0]);

        this._matrixDirty = false;

        // Transform

        this.translate = cfg.translate;
        this.scale = cfg.scale;
        this.rotate = cfg.rotate;

        // Data source

        if (cfg.src) {
            this.src = cfg.src; // Image file
        } else if (cfg.image) {
            this.image = cfg.image; // Image object
        }

        stats.memory.textures++;
    }

    _checkMinFilter(value) {
        value = value || "linearMipmapLinear";
        if (value !== "linear" &&
            value !== "linearMipmapNearest" &&
            value !== "linearMipmapLinear" &&
            value !== "nearestMipmapLinear" &&
            value !== "nearestMipmapNearest") {
            this.error("Unsupported value for 'minFilter': '" + value +
                "' - supported values are 'linear', 'linearMipmapNearest', 'nearestMipmapNearest', " +
                "'nearestMipmapLinear' and 'linearMipmapLinear'. Defaulting to 'linearMipmapLinear'.");
            value = "linearMipmapLinear";
        }
        return value;
    }

    _checkMagFilter(value) {
        value = value || "linear";
        if (value !== "linear" && value !== "nearest") {
            this.error("Unsupported value for 'magFilter': '" + value +
                "' - supported values are 'linear' and 'nearest'. Defaulting to 'linear'.");
            value = "linear";
        }
        return value;
    }

    _checkFilter(value) {
        value = value || "linear";
        if (value !== "linear" && value !== "nearest") {
            this.error("Unsupported value for 'magFilter': '" + value +
                "' - supported values are 'linear' and 'nearest'. Defaulting to 'linear'.");
            value = "linear";
        }
        return value;
    }

    _checkWrapS(value) {
        value = value || "repeat";
        if (value !== "clampToEdge" && value !== "mirroredRepeat" && value !== "repeat") {
            this.error("Unsupported value for 'wrapS': '" + value +
                "' - supported values are 'clampToEdge', 'mirroredRepeat' and 'repeat'. Defaulting to 'repeat'.");
            value = "repeat";
        }
        return value;
    }

    _checkWrapT(value) {
        value = value || "repeat";
        if (value !== "clampToEdge" && value !== "mirroredRepeat" && value !== "repeat") {
            this.error("Unsupported value for 'wrapT': '" + value +
                "' - supported values are 'clampToEdge', 'mirroredRepeat' and 'repeat'. Defaulting to 'repeat'.");
            value = "repeat";
        }
        return value;
    }

    _checkFlipY(value) {
        return !!value;
    }

    _checkEncoding(value) {
        value = value || "linear";
        if (value !== "linear" && value !== "sRGB" && value !== "gamma") {
            this.error("Unsupported value for 'encoding': '" + value + "' - supported values are 'linear', 'sRGB', 'gamma'. Defaulting to 'linear'.");
            value = "linear";
        }
        return value;
    }

    _webglContextRestored() {
        this._state.texture = new Texture2D(this.scene.canvas.gl);
        if (this._image) {
            this.image = this._image;
        } else if (this._src) {
            this.src = this._src;
        }
    }

    _update() {
        const state = this._state;
        if (this._matrixDirty) {
            let matrix;
            let t;
            if (this._translate[0] !== 0 || this._translate[1] !== 0) {
                matrix = math.translationMat4v([this._translate[0], this._translate[1], 0], this._state.matrix);
            }
            if (this._scale[0] !== 1 || this._scale[1] !== 1) {
                t = math.scalingMat4v([this._scale[0], this._scale[1], 1]);
                matrix = matrix ? math.mulMat4(matrix, t) : t;
            }
            if (this._rotate !== 0) {
                t = math.rotationMat4v(this._rotate * 0.0174532925, [0, 0, 1]);
                matrix = matrix ? math.mulMat4(matrix, t) : t;
            }
            if (matrix) {
                state.matrix = matrix;
            }
            this._matrixDirty = false;
        }
        this._renderer.imageDirty();
    }


    /**
     Indicates an HTML DOM Image object to source this Texture from.

     Sets the {{#crossLink "Texture/src:property"}}{{/crossLink}} property to null.

     @property image
     @default null
     @type {HTMLImageElement}
     */
    set image(value) {
        this._image = ensureImageSizePowerOfTwo(value);
        this._image.crossOrigin = "Anonymous";
        this._state.texture.setImage(this._image, this._state);
        this._state.texture.setProps(this._state); // Generate mipmaps
        this._src = null;
        this._renderer.imageDirty();
    }

    get image() {
        return this._image;
    }

    /**
     Indicates a path to an image file to source this Texture from.

     Sets the {{#crossLink "Texture/image:property"}}{{/crossLink}} property to null.

     @property src
     @default null
     @type String
     */
    set src(src) {
        this.scene.loading++;
        this.scene.canvas.spinner.processes++;
        const self = this;
        let image = new Image();
        image.onload = function () {
            image = ensureImageSizePowerOfTwo(image);
            //self._image = image; // For faster WebGL context restore - memory inefficient?
            self._state.texture.setImage(image, self._state);
            self._state.texture.setProps(self._state); // Generate mipmaps
            self.scene.loading--;
            self.scene.canvas.spinner.processes--;
            self._renderer.imageDirty();
        };
        image.src = src;
        this._src = src;
        this._image = null;
    }

    get src() {
        return this._src;
    }

    /**
     2D translation vector that will be added to this Texture's *S* and *T* coordinates.

     @property translate
     @default [0, 0]
     @type Array(Number)
     */
    set translate(value) {
        this._translate.set(value || [0, 0]);
        this._matrixDirty = true;
        this._needUpdate();
    }

    get translate() {
        return this._translate;
    }

    /**
     2D scaling vector that will be applied to this Texture's *S* and *T* coordinates.

     @property scale
     @default [1, 1]
     @type Array(Number)
     */
    set scale(value) {
        this._scale.set(value || [1, 1]);
        this._matrixDirty = true;
        this._needUpdate();
    }

    get scale() {
        return this._scale;
    }

    /**
     Rotation, in degrees, that will be applied to this Texture's *S* and *T* coordinates.

     @property rotate
     @default 0
     @type Number
     */
    set rotate(value) {
        value = value || 0;
        if (this._rotate === value) {
            return;
        }
        this._rotate = value;
        this._matrixDirty = true;
        this._needUpdate();
    }

    get rotate() {
        return this._rotate;
    }

    /**
     How this Texture is sampled when a texel covers less than one pixel.

     Options are:

     * **"nearest"** - Uses the value of the texture element that is nearest
     (in Manhattan distance) to the center of the pixel being textured.

     * **"linear"** - Uses the weighted average of the four texture elements that are
     closest to the center of the pixel being textured.

     * **"nearestMipmapNearest"** - Chooses the mipmap that most closely matches the
     size of the pixel being textured and uses the "nearest" criterion (the texture
     element nearest to the center of the pixel) to produce a texture value.

     * **"linearMipmapNearest"** - Chooses the mipmap that most closely matches the size of
     the pixel being textured and uses the "linear" criterion (a weighted average of the
     four texture elements that are closest to the center of the pixel) to produce a
     texture value.

     * **"nearestMipmapLinear"** - Chooses the two mipmaps that most closely
     match the size of the pixel being textured and uses the "nearest" criterion
     (the texture element nearest to the center of the pixel) to produce a texture
     value from each mipmap. The final texture value is a weighted average of those two
     values.

     * **"linearMipmapLinear"** - **(default)** - Chooses the two mipmaps that most closely match the size
     of the pixel being textured and uses the "linear" criterion (a weighted average
     of the four texture elements that are closest to the center of the pixel) to
     produce a texture value from each mipmap. The final texture value is a weighted
     average of those two values.

     @property minFilter
     @default "linearMipmapLinear"
     @type String
     @final
     */
    get minFilter() {
        return this._state.minFilter;
    }

    /**
     How this Texture is sampled when a texel covers more than one pixel.

     Options are:

     * **"nearest"** - Uses the value of the texture element that is nearest
     (in Manhattan distance) to the center of the pixel being textured.
     * **"linear"** - **(default)** - Uses the weighted average of the four texture elements that are
     closest to the center of the pixel being textured.

     @property magFilter
     @default "linear"
     @type String
     @final
     */
    get magFilter() {
        return this._state.magFilter;
    }

    /**
     Wrap parameter for this Texture's *S* coordinate.

     Options are:

     * **"clampToEdge"** -  causes *S* coordinates to be clamped to the size of the texture.
     * **"mirroredRepeat"** - causes the *S* coordinate to be set to the fractional part of the texture coordinate
     if the integer part of *S* is even; if the integer part of *S* is odd, then the *S* texture coordinate is
     set to *1 - frac ⁡ S* , where *frac ⁡ S* represents the fractional part of *S*.
     * **"repeat"** - **(default)** - causes the integer part of the *S* coordinate to be ignored; xeogl uses only the
     fractional part, thereby creating a repeating pattern.

     @property wrapS
     @default "repeat"
     @type String
     @final
     */
    get wrapS() {
        return this._state.wrapS;
    }

    /**
     Wrap parameter for this Texture's *T* coordinate.

     Options are:

     * **"clampToEdge"** -  Causes *T* coordinates to be clamped to the size of the texture.
     * **"mirroredRepeat"** - Causes the *T* coordinate to be set to the fractional part of the texture coordinate
     if the integer part of *T* is even; if the integer part of *T* is odd, then the *T* texture coordinate is
     set to *1 - frac ⁡ S* , where *frac ⁡ S* represents the fractional part of *T*.
     * **"repeat"** - **(default)** - Causes the integer part of the *T* coordinate to be ignored; xeogl uses only the
     fractional part, thereby creating a repeating pattern.

     @property wrapT
     @default "repeat"
     @type String
     @final
     */
    get wrapT() {
        return this._state.wrapT;
    }

    /**
     Flips this Texture's source data along its vertical axis when true.

     @property flipY
     @type Boolean
     @final
     */
    get flipY() {
        return this._state.flipY;
    }

    /**
     The Texture's encoding format.

     @property encoding
     @type String
     @final
     */
    get encoding() {
        return this._state.encoding;
    }

    destroy() {
        super.destroy();
        if (this._state.texture) {
            this._state.texture.destroy();
        }
        this._state.destroy();
        stats.memory.textures--;
    }
}

componentClasses[type] = Texture;

export{Texture};