Source: ShaderLens.js

import { Shader } from './Shader.js'

/**
 * @typedef {Object} ShaderLens~Uniforms
 * Uniform definitions for lens shader
 * @property {number[]} u_lens - Lens parameters [centerX, centerY, radius, borderWidth]
 * @property {number[]} u_width_height - Viewport dimensions [width, height]
 * @property {number[]} u_border_color - RGBA border color [r, g, b, a]
 * @property {boolean} u_border_enable - Whether to show lens border
 */

/**
 * @typedef {Object} ShaderLens~Options
 * Configuration options for lens shader
 * @property {string} [label='ShaderLens'] - Display label
 * @property {boolean} [overlayLayerEnabled=false] - Enable overlay layer
 * @property {Object} [uniforms] - Custom uniform values
 * @extends Shader~Options
 */

/**
 * ShaderLens implements a circular magnification lens effect with optional overlay.
 * 
 * Features:
 * - Circular lens with smooth borders
 * - Configurable lens size and position
 * - Optional border with customizable color
 * - Optional overlay layer with grayscale outside lens
 * - Smooth transition between lens and background
 * - Real-time lens movement
 * 
 * Technical Implementation:
 * - Pixel-based distance calculations
 * - Smooth border transitions
 * - Alpha blending for overlays
 * - WebGL 1.0 and 2.0 compatibility
 * - Viewport coordinate mapping
 * 
 *
 * Example usage:
 * ```javascript
 * // Create lens shader
 * const lens = new ShaderLens();
 * 
 * // Configure lens
 * lens.setLensUniforms(
 *     [400, 300, 100, 10],  // center at (400,300), radius 100, border 10
 *     [800, 600],           // viewport size
 *     [0.8, 0.8, 0.8, 1],   // gray border
 *     true                  // show border
 * );
 * 
 * // Enable overlay
 * lens.setOverlayLayerEnabled(true);
 * ```
 * 
 * Advanced usage with custom configuration:
 * ```javascript
 * const lens = new ShaderLens({
 *     uniforms: {
 *         u_lens: { value: [0, 0, 150, 15] },
 *         u_border_color: { value: [1, 0, 0, 1] }  // red border
 *     },
 *     overlayLayerEnabled: true
 * });
 * ```
 *
 * GLSL Implementation Details
 * 
 * Key Components:
 * 1. Lens Function:
 *    - Distance-based circle calculation
 *    - Smooth border transitions
 *    - Color mixing and blending
 * 
 * 2. Overlay Processing:
 *    - Grayscale conversion
 *    - Alpha blending
 *    - Border preservation
 * 
 * Functions:
 * - lensColor(): Handles color transitions between lens regions
 * - data(): Main processing function
 * 
 * Uniforms:
 * - {vec4} u_lens - Lens parameters [cx, cy, radius, border]
 * - {vec2} u_width_height - Viewport dimensions
 * - {vec4} u_border_color - Border color and alpha
 * - {bool} u_border_enable - Border visibility flag
 * - {sampler2D} source0 - Main texture
 * - {sampler2D} source1 - Optional overlay texture
 *
 * @extends Shader
 */
class ShaderLens extends Shader {
    /**
     * Creates a new lens shader
     * @param {ShaderLens~Options} [options] - Configuration options
     * 
     * @example
     * ```javascript
     * // Create basic lens shader
     * const lens = new ShaderLens({
     *     label: 'MyLens',
     *     overlayLayerEnabled: false
     * });
     * ```
     */
    constructor(options) {
        super(options);

        this.samplers = [
            { id: 0, name: 'source0' }, { id: 1, name: 'source1' }
        ];

        this.uniforms = {
            u_lens: { type: 'vec4', needsUpdate: true, size: 4, value: [0, 0, 100, 10] },
            u_width_height: { type: 'vec2', needsUpdate: true, size: 2, value: [1, 1] },
            u_border_color: { type: 'vec4', needsUpdate: true, size: 4, value: [0.8, 0.8, 0.8, 1] },
            u_border_enable: { type: 'bool', needsUpdate: true, size: 1, value: false }
        };
        this.label = "ShaderLens";
        this.needsUpdate = true;
        this.overlayLayerEnabled = false;
    }

    /**
     * Enables or disables the overlay layer
     * When enabled, adds a second texture layer with grayscale outside lens
     * @param {boolean} enabled - Whether to enable overlay
     */
    setOverlayLayerEnabled(x) {
        this.overlayLayerEnabled = x;
        this.needsUpdate = true;
    }

    /**
     * Updates lens parameters and appearance
     * @param {number[]} lensViewportCoords - Lens parameters [centerX, centerY, radius, borderWidth]
     * @param {number[]} windowWH - Viewport dimensions [width, height]
     * @param {number[]} borderColor - RGBA border color
     * @param {boolean} borderEnable - Whether to show border
     */
    setLensUniforms(lensViewportCoords, windowWH, borderColor, borderEnable) {
        this.setUniform('u_lens', lensViewportCoords);
        this.setUniform('u_width_height', windowWH);
        this.setUniform('u_border_color', borderColor);
        this.setUniform('u_border_enable', borderEnable);
    }

    /**
     * Generates fragment shader source code.
     * 
     * Shader Features:
     * - Circular lens implementation
     * - Smooth border transitions
     * - Optional overlay support
     * - Grayscale conversion outside lens
     * 
     * @param {WebGLRenderingContext} gl - WebGL context
     * @returns {string} Fragment shader source code
     * @private
     */
    fragShaderSrc(gl) {
        let gl2 = !(gl instanceof WebGLRenderingContext);

        let samplerDeclaration = `uniform sampler2D ` + this.samplers[0].name + `;`;
        let overlaySamplerCode = "";

        if (this.overlayLayerEnabled) { //FIXME two cases with transparence or not.
            samplerDeclaration += `uniform sampler2D ` + this.samplers[1].name + `;`;

            overlaySamplerCode =
                `vec4 c1 = texture${gl2 ? '' : '2D'}(source1, v_texcoord);
            if (r > u_lens.z) {
                float k = (c1.r + c1.g + c1.b) / 3.0;
                c1 = vec4(k, k, k, c1.a);
            } else if (u_border_enable && r > innerBorderRadius) {
                // Preserve border keeping c1 alpha at zero
                c1.a = 0.0; 
            }
            color = color * (1.0 - c1.a) + c1 * c1.a;
            `
        }
        return `

        ${samplerDeclaration}
        uniform vec4 u_lens; // [cx, cy, radius, border]
        uniform vec2 u_width_height; // Keep wh to map to pixels. TexCoords cannot be integer unless using texture_rectangle
        uniform vec4 u_border_color;
        uniform bool u_border_enable;
        ${gl2 ? 'in' : 'varying'} vec2 v_texcoord;

        vec4 lensColor(in vec4 c_in, in vec4 c_border, in vec4 c_out,
            float r, float R, float B) {
            vec4 result;
            if (u_border_enable) {
                float B_SMOOTH = B < 8.0 ? B/8.0 : 1.0;
                if (r<R-B+B_SMOOTH) {
                    float t=smoothstep(R-B, R-B+B_SMOOTH, r);
                    result = mix(c_in, c_border, t);
                } else if (r<R-B_SMOOTH) {
                    result = c_border;  
                } else {
                    float t=smoothstep(R-B_SMOOTH, R, r);
                    result = mix(c_border, c_out, t);
                }
            } else {
                result = (r<R) ? c_in : c_out;
            }
            return result;
        }

        vec4 data() {
            vec4 color;
            float innerBorderRadius = (u_lens.z - u_lens.w);
            float dx = v_texcoord.x * u_width_height.x - u_lens.x;
            float dy = v_texcoord.y * u_width_height.y - u_lens.y;
            float r = sqrt(dx*dx + dy*dy);

            vec4 c_in = texture${gl2 ? '' : '2D'}(source0, v_texcoord);
            vec4 c_out = u_border_color; c_out.a=0.0;
            
            color = lensColor(c_in, u_border_color, c_out, r, u_lens.z, u_lens.w);

            ${overlaySamplerCode}
            return color;
        }
        `
    }

    /**
     * Generates vertex shader source code.
     * 
     * @param {WebGLRenderingContext} gl - WebGL context
     * @returns {string} Vertex shader source code
     * @private
     */
    vertShaderSrc(gl) {
        let gl2 = !(gl instanceof WebGLRenderingContext);
        return `${gl2 ? '#version 300 es' : ''}
 

${gl2 ? 'in' : 'attribute'} vec4 a_position;
${gl2 ? 'in' : 'attribute'} vec2 a_texcoord;

${gl2 ? 'out' : 'varying'} vec2 v_texcoord;
void main() {
	gl_Position = a_position;
    v_texcoord = a_texcoord;
}`;
    }
}

export { ShaderLens }