Source: ShaderFilter.js

/**
 * @typedef {Object} ShaderFilter~Mode
 * A shader filter mode configuration
 * @property {string} id - Unique identifier for the mode
 * @property {boolean} enable - Whether the mode is active
 * @property {string} src - GLSL source code for the mode
 */

/**
 * @typedef {Object} ShaderFilter~Sampler
 * A texture sampler used by the filter
 * @property {string} name - Unique name for the sampler
 * @property {WebGLTexture} [texture] - Associated WebGL texture
 * @property {WebGLUniformLocation} [location] - GPU location for the sampler
 */

/**
 * 
 * Base class for WebGL shader filters in OpenLIME.
 * Provides infrastructure for creating modular shader effects that can be chained together.
 * 
 * Features:
 * - Modular filter architecture
 * - Automatic uniform management
 * - Dynamic mode switching
 * - Texture sampling support
 * - GLSL code generation
 * - Unique naming conventions
 * 
 * Technical Implementation:
 * - Generates unique names for uniforms and samplers
 * - Manages WebGL resource lifecycle
 * - Supports multiple filter modes
 * - Handles shader program integration
 */
class ShaderFilter {
    /**
     * Creates a new shader filter
     * @param {Object} [options] - Filter configuration
     * @param {ShaderFilter~Mode} [options.modes={}] - Available filter modes
     * @param {Object} [options.uniforms={}] - Filter uniform variables
     * @param {Array<ShaderFilter~Sampler>} [options.samplers=[]] - Texture samplers
     */
    constructor(options) {
        options = Object.assign({
        }, options);
        Object.assign(this, options);
        this.name = this.constructor.name;
        this.uniforms = {};
        this.samplers = [];
        this.needsUpdate = true;
        this.shader = null;

        this.modes = {};
    }

    /**
     * Sets the active mode for the filter
     * @param {string} mode - Mode category to modify
     * @param {string} id - Specific mode ID to enable
     * @throws {Error} If shader not registered or mode doesn't exist
     */
    setMode(mode, id) {
        if (!this.shader)
            throw Error("Shader not registered");

        if (Object.keys(this.modes).length > 0) {
            const list = this.modes[mode];
            if (list) {
                list.map(a => {
                    a.enable = a.id == id;
                });
                this.shader.needsUpdate = true;
            } else {
                throw Error(`Mode "${mode}" not exist!`);
            }
        }
    }

    /**
     * Prepares filter resources for rendering
     * @param {WebGLRenderingContext} gl - WebGL context
     * @private
     */
    prepare(gl) {
        if (this.needsUpdate)
            if (this.createTextures) this.createTextures(gl);
        this.needsUpdate = false;
    }


    /**
     * Generates mode-specific GLSL code
     * @returns {string} GLSL declarations for enabled modes
     * @private
     */
    fragModeSrc() {
        let src = '';
        for (const key of Object.keys(this.modes)) {
            for (const e of this.modes[key]) {
                if (e.enable) {
                    src += e.src + '\n';
                }
            }
        }
        return src;
    }

    /**
     * Sets a uniform variable value
     * @param {string} name - Base name of uniform variable
     * @param {number|boolean|Array} value - Value to set
     * @throws {Error} If shader not registered
     */
    setUniform(name, value) {
        if (!this.shader) {
            throw Error(`Shader not registered`);
        }
        this.shader.setUniform(this.uniformName(name), value);
    }

    /**
     * Generates sampler declarations
     * @returns {string} GLSL sampler declarations
     * @private
     */
    fragSamplerSrc() {
        let src = '';
        for (let s of this.samplers) {
            src += `
            uniform sampler2D ${s.name};`;
        }
        return src;
    }

    /**
     * Generates uniform variable declarations
     * @returns {string} GLSL uniform declarations
     * @private
     */
    fragUniformSrc() {
        let src = '';
        for (const [key, value] of Object.entries(this.uniforms)) {
            src += `
            uniform ${this.uniforms[key].type} ${key};`;
        }
        return src;
    }

    /**
     * Generates filter-specific GLSL function
     * @param {WebGLRenderingContext} gl - WebGL context
     * @returns {string|null} GLSL function definition
     * @virtual
     */
    fragDataSrc(gl) {
        return null;
    }

    // Utility methods documentation
    /**
     * @returns {string} Generated function name for the filter
     * @private
     */
    functionName() {
        return this.name + "_data";
    }

    /**
     * @param {string} name - Base sampler name
     * @returns {string} Unique sampler identifier
     * @private
     */
    samplerName(name) {
        return `${this.name}_${name}`;
    }

    /**
     * @param {string} name - Base uniform name
     * @returns {string} Unique uniform identifier
     * @private
     */
    uniformName(name) {
        return `u_${this.name}_${name}`;
    }

    /**
     * @param {string} name - Base mode name
     * @returns {string} Unique mode identifier
     * @private
     */
    modeName(name) {
        return `m_${this.name}_${name}`;
    }

    /**
     * Finds a sampler by name
     * @param {string} name - Base sampler name
     * @returns {ShaderFilter~Sampler|undefined} Found sampler or undefined
     */
    getSampler(name) {
        const samplername = this.samplerName(name);
        return this.samplers.find(e => e.name == samplername);
    }
}

/**
 * 
 * @extends ShaderFilter
 * Test filter that replaces transparent pixels with a specified color
 */
class ShaderFilterTest extends ShaderFilter {
    /**
     * Creates a test filter
     * @param {Object} [options] - Filter options
     * @param {number[]} [options.nodata_col=[1,1,0,1]] - Color for transparent pixels
     */
    constructor(options) {
        super(options);
        this.uniforms[this.uniformName('nodata_col')] = { type: 'vec4', needsUpdate: true, size: 4, value: [1, 1, 0, 1] };
    }

    fragDataSrc(gl) {
        return `
            vec4 ${this.functionName()}(vec4 col){
                return col.a > 0.0 ? col : ${this.uniformName('nodata_col')};
            }`;
    }
}

/**
 * 
 * @extends ShaderFilter
 * Filter that modifies the opacity of rendered content
 */
class ShaderFilterOpacity extends ShaderFilter {
    /**
     * Creates an opacity filter
     * @param {number} opacity - Initial opacity value [0-1]
     * @param {Object} [options] - Additional filter options
     */    
    constructor(opacity, options) {
        super(options);
        this.uniforms[this.uniformName('opacity')] = { type: 'float', needsUpdate: true, size: 1, value: opacity };
    }

    fragDataSrc(gl) {
        return `
            vec4 ${this.functionName()}(vec4 col){
                return vec4(col.rgb, col.a * ${this.uniformName('opacity')});
            }`;
    }
}

/**
 * 
 * @extends ShaderFilter
 * Filter that applies gamma correction to colors
 */
class ShaderGammaFilter extends ShaderFilter {
    /**
     * Creates a gamma correction filter
     * @param {Object} [options] - Filter options
     * @param {number} [options.gamma=2.2] - Gamma correction value
     */    
    constructor(options) {
        super(options);
        this.uniforms[this.uniformName('gamma')] = { type: 'float', needsUpdate: true, size: 1, value: 2.2 };
    }

    fragDataSrc(gl) {
        return `
            vec4 ${this.functionName()}(vec4 col){
                float igamma = 1.0/${this.uniformName('gamma')};
                return vec4(pow(col.r, igamma), pow(col.g, igamma), pow(col.b, igamma), col.a);
            }`;
    }
}

export { ShaderFilter, ShaderFilterTest, ShaderFilterOpacity, ShaderGammaFilter }